CapTube

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

目前為 2016-12-19 提交的版本,檢視 最新版本

  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.6
  8. // @grant none
  9. // @license public domain
  10. // @noframes
  11. // ==/UserScript==
  12.  
  13. (function() {
  14.  
  15. let previewContainer = null, meterContainer = 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 callOnIdle = function(func) {
  39. if (window.requestIdleCallback) {
  40. window.requestIdleCallback(func);
  41. } else {
  42. setTimeout(func, 0);
  43. }
  44. };
  45.  
  46. const DataUrlConv = (function() {
  47. const sessions = {};
  48.  
  49. const func = function(self) {
  50. self.onmessage = function(e) {
  51. const dataURL = e.data.dataURL;
  52. const sessionId = e.data.sessionId;
  53.  
  54. const bin = atob(dataURL.split(',')[1]);
  55. const buf = new Uint8Array(bin.length);
  56.  
  57. for (let i = 0, len = buf.length; i < len; i++) {
  58. buf[i] = bin.charCodeAt(i);
  59. }
  60.  
  61. const blob = new Blob([buf.buffer], {type: 'image/png'});
  62. const objectURL = URL.createObjectURL(blob);
  63. self.postMessage({objectURL, sessionId});
  64. };
  65. };
  66.  
  67. const worker = createWebWorker(func);
  68. worker.addEventListener('message', (e) => {
  69. const sessionId = e.data.sessionId;
  70. if (!sessions[sessionId]) { return; }
  71.  
  72. (sessions[sessionId])(e.data.objectURL);
  73. delete sessions[sessionId];
  74. });
  75.  
  76. return {
  77. toObjectURL: function(dataURL) {
  78. return new Promise(resolve => {
  79. const sessionId = 'id:' + Math.random();
  80. sessions[sessionId] = resolve;
  81. worker.postMessage({dataURL, sessionId});
  82. });
  83. }
  84. };
  85. })();
  86.  
  87.  
  88. const __css__ = (`
  89. #CapTubePreviewContainer {
  90. position: fixed;
  91. padding: 16px 0 0 16px;
  92. width: 90%;
  93. bottom: 100px;
  94. left: 5%;
  95. z-index: 10000;
  96. pointer-events: none;
  97. transform: translateZ(0);
  98. /*background: rgba(192, 192, 192, 0.4);*/
  99. border: 1px solid #ccc;
  100. -webkit-user-select: none;
  101. user-select: none;
  102. }
  103.  
  104. #CapTubePreviewContainer:empty {
  105. display: none;
  106. }
  107. #CapTubePreviewContainer canvas {
  108. display: inline-block;
  109. width: 256px;
  110. margin-right: 16px;
  111. margin-bottom: 16px;
  112. outline: solid 1px #ccc;
  113. outline-offset: 4px;
  114. transform: translateZ(0);
  115. transition:
  116. 1s opacity linear,
  117. 1s margin-right linear;
  118. }
  119.  
  120. #CapTubePreviewContainer canvas.is-removing {
  121. opacity: 0;
  122. margin-right: -272px;
  123. /*width: 0;*/
  124. }
  125.  
  126. #CapTubeMeterContainer {
  127. pointer-events: none;
  128. position: fixed;
  129. width: 26px;
  130. bottom: 100px;
  131. left: 16px;
  132. z-index: 10000;
  133. border: 1px solid #ccc;
  134. transform: translateZ(0);
  135. -webkit-user-select: none;
  136. user-select: none;
  137. }
  138.  
  139. #CapTubeMeterContainer::after {
  140. content: 'queue';
  141. position: absolute;
  142. bottom: -2px;
  143. left: 50%;
  144. transform: translate(-50%, 100%);
  145. color: #666;
  146. }
  147.  
  148. #CapTubeMeterContainer:empty {
  149. display: none;
  150. }
  151.  
  152. #CapTubeMeterContainer .memory {
  153. display: block;
  154. width: 24px;
  155. height: 8px;
  156. margin: 1px 0 0;
  157. background: darkgreen;
  158. opacity: 0.5;
  159. border: 1px solid #ccc;
  160. }
  161.  
  162. `).trim();
  163.  
  164. addStyle(__css__);
  165.  
  166. const getVideoId = function() {
  167. var id = '';
  168. location.search.substring(1).split('&').forEach(function(item){
  169. if (item.split('=')[0] === 'v') { id = item.split('=')[1]; }
  170. });
  171. return id;
  172. };
  173.  
  174. const toSafeName = function(text) {
  175. text = text.trim()
  176. .replace(/</g, '<')
  177. .replace(/>/g, '>')
  178. .replace(/\?/g, '?')
  179. .replace(/:/g, ':')
  180. .replace(/\|/g, '|')
  181. .replace(/\//g, '/')
  182. .replace(/\\/g, '¥')
  183. .replace(/"/g, '”')
  184. .replace(/\./g, '.')
  185. ;
  186. return text;
  187. };
  188.  
  189. const getVideoTitle = function() {
  190. var prefix = localStorage['CapTube-prefix'] || '';
  191. var videoId = getVideoId();
  192. var title = document.querySelector('.watch-title');
  193. var authorName = toSafeName(document.querySelector('.yt-user-info a').text);
  194. var titleText = toSafeName(title.textContent);
  195. titleText = prefix + titleText + ' - by ' + authorName + ' (v=' + videoId + ')';
  196.  
  197. return titleText;
  198. };
  199.  
  200. const createCanvasFromVideo = function(video) {
  201. console.time('createCanvasFromVideo');
  202. const width = video.videoWidth;
  203. const height = video.videoHeight;
  204. const canvas = document.createElement('canvas');
  205. canvas.width = width;
  206. canvas.height = height;
  207. const context = canvas.getContext('2d');
  208. context.drawImage(video, 0, 0);
  209.  
  210.  
  211. const thumbnail = document.createElement('canvas');
  212. thumbnail.width = 256;
  213. thumbnail.height = canvas.height * (256 / canvas.width);
  214. thumbnail.getContext('2d').drawImage(canvas, 0, 0, thumbnail.width, thumbnail.height);
  215. console.timeEnd('createCanvasFromVideo');
  216.  
  217. return {canvas, thumbnail};
  218. };
  219.  
  220. const getFileName = function(video) {
  221. const title = getVideoTitle();
  222. const currentTime = video.currentTime;
  223. const min = Math.floor(currentTime / 60);
  224. const sec = (currentTime % 60 + 100).toString().substr(1, 6);
  225. const time = `${min}_${sec}`;
  226.  
  227. return `${title}@${time}.png`;
  228. };
  229. /*
  230. const createBlobLinkElement = function(canvas, fileName) {
  231. console.time('createBlobLinkElement');
  232.  
  233. console.time('canvas.toDataURL');
  234. const dataURL = canvas.toDataURL('image/png');
  235. console.timeEnd('canvas.toDataURL');
  236.  
  237. console.time('createObjectURL');
  238. const bin = atob(dataURL.split(',')[1]);
  239. const buf = new Uint8Array(bin.length);
  240. for (let i = 0, len = buf.length; i < len; i++) { buf[i] = bin.charCodeAt(i); }
  241. const blob = new Blob([buf.buffer], {type: 'image/png'});
  242. const url = window.URL.createObjectURL(blob);
  243. console.timeEnd('createObjectURL');
  244.  
  245. const link = document.createElement('a');
  246. link.setAttribute('download', fileName);
  247. link.setAttribute('target', '_blank');
  248. link.setAttribute('href', url);
  249.  
  250. console.timeEnd('createBlobLinkElement');
  251. return link;
  252. };
  253. */
  254.  
  255. const createBlobLinkElementAsync = function(canvas, fileName) {
  256. //console.time('createBlobLinkElement');
  257.  
  258. console.time('canvas to DataURL');
  259. const dataURL = canvas.toDataURL('image/png');
  260. console.timeEnd('canvas to DataURL');
  261.  
  262. console.time('dataURL to objectURL');
  263.  
  264. return DataUrlConv.toObjectURL(dataURL).then(objectURL => {
  265. console.timeEnd('dataURL to objectURL');
  266.  
  267. const link = document.createElement('a');
  268. link.setAttribute('download', fileName);
  269. link.setAttribute('target', '_blank');
  270. link.setAttribute('href', objectURL);
  271.  
  272. //console.timeEnd('createBlobLinkElement');
  273. return Promise.resolve(link);
  274. });
  275. };
  276.  
  277. const saveScreenShot = function() {
  278. const video = document.querySelector('.html5-main-video');
  279. if (!video) { return; }
  280.  
  281. const meter = document.createElement('div');
  282. if (meterContainer) {
  283. meter.className = 'memory';
  284. meterContainer.appendChild(meter);
  285. }
  286.  
  287. const {canvas, thumbnail} = createCanvasFromVideo(video);
  288. const fileName = getFileName(video);
  289.  
  290. const create = () => {
  291. createBlobLinkElementAsync(canvas, fileName).then(link => {
  292. document.body.appendChild(link);
  293. link.click();
  294. setTimeout(() => {
  295. link.remove();
  296. meter.remove();
  297. URL.revokeObjectURL(link.getAttribute('href'));
  298. }, 1000);
  299. });
  300. };
  301.  
  302. callOnIdle(create);
  303.  
  304. if (!previewContainer) { return; }
  305. previewContainer.appendChild(thumbnail);
  306. setTimeout(() => {
  307. thumbnail.classList.add('is-removing');
  308. setTimeout(() => { thumbnail.remove(); }, 2000);
  309. }, 1500);
  310. };
  311.  
  312. const setPlaybackRate = function(v) {
  313. const video = document.querySelector('.html5-main-video');
  314. if (!video) { return; }
  315. video.playbackRate = v;
  316. };
  317.  
  318. const togglePlay = function() {
  319. const video = document.querySelector('.html5-main-video');
  320. if (!video) { return; }
  321.  
  322. if (video.paused) {
  323. video.play();
  324. } else {
  325. video.pause();
  326. }
  327. };
  328.  
  329. const seekBy = function(v) {
  330. const video = document.querySelector('.html5-main-video');
  331. if (!video) { return; }
  332.  
  333. const ct = Math.max(video.currentTime + v, 0);
  334. video.currentTime = ct;
  335. };
  336.  
  337. let isVerySlow = false;
  338. const onKeyDown = (e) => {
  339. const key = e.key.toLowerCase();
  340. switch (key) {
  341. case 'd':
  342. setPlaybackRate(0.1);
  343. isVerySlow = true;
  344. break;
  345. case 's':
  346. saveScreenShot();
  347. break;
  348. }
  349. };
  350.  
  351. const onKeyUp = (e) => {
  352. //console.log('onKeyUp', e);
  353. const key = e.key.toLowerCase();
  354. switch (key) {
  355. case 'd':
  356. setPlaybackRate(1);
  357. isVerySlow = false;
  358. break;
  359. }
  360. };
  361.  
  362. const onKeyPress = (e) => {
  363. const key = e.key.toLowerCase();
  364. switch (key) {
  365. case 'w':
  366. togglePlay();
  367. break;
  368. case 'a':
  369. seekBy(isVerySlow ? -0.5 : -5);
  370. break;
  371. }
  372. };
  373.  
  374.  
  375. const initDom = function() {
  376. const div = document.createElement('div');
  377. div.id = 'CapTubePreviewContainer';
  378. document.body.appendChild(div);
  379. previewContainer = div;
  380.  
  381. meterContainer = document.createElement('div');
  382. meterContainer.id = 'CapTubeMeterContainer';
  383. document.body.appendChild(meterContainer);
  384. };
  385.  
  386. const initialize = function() {
  387. initDom();
  388. window.addEventListener('keydown', onKeyDown);
  389. window.addEventListener('keyup', onKeyUp);
  390. window.addEventListener('keypress', onKeyPress);
  391. };
  392.  
  393. initialize();
  394. })();