// ==UserScript== // @name HTML5视频截图器 // @namespace indefined // @supportURL https://github.com/indefined/UserScripts/issues // @version 0.3.8 // @description 基于HTML5的简单任意原生视频截图,可简单控制快进/逐帧/视频调速 // @author indefined // @include *://* // @run-at document-idle // @grant GM_registerMenuCommand // @license MIT // @downloadURL none // ==/UserScript== function HTML5VideoCapturer(){ 'use strict'; if (document.querySelector('#HTML5VideoCapture')) return; const childs = "undefined"==typeof(unsafeWindow)?window.frames:unsafeWindow.frames; let videos,video,selectId; function videoShot(down){ if (!video) return postMsg('shot',down); const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d') .drawImage(video, 0, 0, canvas.width, canvas.height); try{ if (!down) throw `i don't want to do it.`; const a = document.createElement('a'); a.href = canvas.toDataURL('image/jpeg', 0.95); a.download = `${document.title}_${Math.floor(video.currentTime/60)}'${(video.currentTime%60).toFixed(3)}''.jpg`; document.head.appendChild(a); a.click(); document.head.removeChild(a); }catch(e){ const imgWin = open("",'_blank'); canvas.style = "max-width:100%"; imgWin.document.body.appendChild(canvas); } } function videoPlay(){ if (!video) return postMsg('play'); video.paused?video.play():video.pause(); videoStatusUpdate(); } function videoSpeedChange(speed){ if (!video) return postMsg('speed',speed); video.playbackRate = speed; videoStatusUpdate(); } function videoStep(offset){ if (!video) return postMsg('step',offset); if (Math.abs(offset)<1&&!video.paused) videoPlay(); video.currentTime += offset; if(video.currentTime<0) video.currentTime = 0; } function videoDetech(){ videos = document.querySelectorAll('video'); if (window!=top){ top.postMessage({ action:'captureReport', about:'videoNums', length:videos.length, id:window.captureId },'*'); }else{ while(selector.firstChild) selector.removeChild(selector.firstChild); appendVideo(videos); setTimeout(()=>{ if (selector.childNodes.length) return videoSelect(selector.value); const toast = document.createElement('div'); toast.style = `position: fixed;top: 50%;left: 50%;z-index: 999999;padding: 10px;background: darkcyan;transform: translate(-50%);color: #fff;border-radius: 6px;` toast.innerText = '当前页面没有检测到HTML5视频'; document.body.appendChild(toast); setTimeout(()=>toast.remove(),2000); },100); } if (childs.length){ [].forEach.call(childs,(w,i)=>w.postMessage({ action:'captureDetech', id:window.captureId==undefined?i:window.captureId+'-'+i },'*')); } console.log(window.captureId,videos); } function videoSelect(id){ selectId = id; if (videos[id]){ video = videos[id]; video.scrollIntoView(); videoStatusUpdate(); } else { video = undefined; postMsg('select'); } } function videoStatusUpdate(){ if (window==top) { play.innerText = video.paused?"▶":"❚❚"; speed.value = video.playbackRate; } else{ top.postMessage({ action:'captureReport', about:'videoStatus', paused:video.paused, speed:video.playbackRate, id:window.captureId },'*'); } } function postMsg(type,data){ if (selectId==undefined||selectId=='') return; const ids = selectId.split('-'); if (ids.length>1){ const target = ids.shift(); if (!childs[target]) return; childs[target].postMessage({ action:'captureControl', target:window.captureId==undefined?target:window.captureId+'-'+target, todo:type, id:ids.join('-'), value:data },'*'); } } //控制事件接收仅在iframe中执行 if (window!=top) { window.addEventListener('message', function(ev) { //console.info('frame recive:',ev.data); if (ev.source!=window.parent || !ev.data.action) return; else if(ev.data.action=='captureDetech'){ window.captureId = ev.data.id; videoDetech(); }else if(ev.data.action=='captureControl' && ev.data.target==window.captureId){ switch (ev.data.todo){ case 'play': videoPlay(ev.data.value); break; case 'shot': videoShot(ev.data.value); break; case 'step': videoStep(ev.data.value); break; case 'speed': videoSpeedChange(ev.data.value); break; case 'select': videoSelect(ev.data.id); break; default: break; } } }); return; } //以下UI控制界面及事件在iframe中不执行 let panel,selector,speed,play; function topReciver(ev) { //console.info('top recive:',ev.data); if (ev.data.action!='captureReport') return; if (ev.data.about=='videoNums') appendVideo(ev.data); else if (ev.data.about=='videoStatus'&&selector.value.startsWith(ev.data.id)){ play.innerText = ev.data.paused?"▶":"❚❚"; speed.value = ev.data.speed; } } function _c(config){ if(config instanceof Array) return config.map(_c); const item = document.createElement(config.nodeType); for(const i in config){ if(i=='nodeType') continue; if(i=='childs' && config.childs instanceof Array) { config.childs.forEach(child=>{ if(child instanceof HTMLElement) item.appendChild(child); else item.appendChild(_c(child)); }) continue; } else if(i=='parent') { config.parent.appendChild(item); continue; } item[i] = config[i]; } return item; } function appendVideo(v){ if (v&&v.length){ for (let i=0;i*{margin:0 0 5px 10px;}' + 'div#HTML5VideoCapture>span,div#HTML5VideoCapture>span>*{white-space:nowrap;}' + 'div#HTML5VideoCapture *{font-family:initial;color:#fff;background:transparent;line-height:20px;height:20px;box-sizing:content-box;vertical-align:top;}' + 'div#HTML5VideoCapture .h5vc-block {border:1px solid #ffffff99;border-radius:2px;padding:1px 4px;min-width:unset;}' + 'div#HTML5VideoCapture .h5vc-block:hover {border-color: #fff;}' }, { nodeType:'div', innerText:'HTML5视频截图工具', style:'cursor:move;user-select:none;font-size:14px;height:auto;padding-left:0;min-width:60px;margin-right:10px;', onmousedown:dialogMove, ondblclick:()=>{ speed.step = 0.25; videoSpeedChange(speed.value=1); } }, { nodeType:'button', className:'h5vc-block', style:'width:20px', innerText:'⟲', title:'重新检测页面中的视频', onclick:videoDetech }, selector = _c({ nodeType:'select', className:'h5vc-block', title:'选择视频', style:'width:unset;min-width:30px', onchange: ()=>videoSelect(selector.value) }), speed = _c({ nodeType:'input', className:'h5vc-block', type:'number',step:0.25,min:0, title:'视频速度,双击截图工具标题恢复原速', style:'width:40px;', oninput:()=>{ speed.step = speed.value<1?0.1:0.25; videoSpeedChange(+speed.value); } }), play = _c({ nodeType:'button', className:'h5vc-block', style:'width:24px', innerText:'▶', onclick:videoPlay }), { nodeType:'button', className:'h5vc-block', innerText:'<<', title:'后退1s,按住shift 5s,ctrl 10s,alt 60s,多按相乘', onclick:e=>{ let offset = -1; if(e.ctrlKey) offset *= 5; if(e.shiftKey) offset *= 10; if(e.altKey) offset *= 60; videoStep(offset); } }, { nodeType:'button', className:'h5vc-block', style:'margin-left:0', innerText:'<', title:'上一帧(1/60s)', onclick:()=>videoStep(-1/60) }, { nodeType:'button', className:'h5vc-block', style:'width:20px', innerText:'⚫', title:'新建标签页打开视频截图', onclick:()=>videoShot() }, { nodeType:'button', className:'h5vc-block', style:'margin-left:0', innerText:'↓', title:'直接下载截图(如果可用)', onclick:()=>videoShot(true) }, { nodeType:'button', className:'h5vc-block', innerText:'>', title:'下一帧(1/60s)', onclick:()=>videoStep(1/60) }, { nodeType:'button', className:'h5vc-block', style:'margin-left:0', innerText:'>>', title:'前进1s,按住shift 5s,ctrl 10s,alt 60s,多按相乘', onclick:e=>{ let offset = 1; if(e.ctrlKey) offset *= 5; if(e.shiftKey) offset *= 10; if(e.altKey) offset *= 60; videoStep(offset); } }, { nodeType:'button', className:'h5vc-block', innerText:'⏏', title:'关闭截图工具栏', style:'margin-right:10px;width:20px', onclick:()=> { document.body.removeChild(panel); window.removeEventListener('message', topReciver); } } ], parent:document.body }); window.addEventListener('message', topReciver); videoDetech(); } if ('function'==typeof(GM_registerMenuCommand) && window==top){ GM_registerMenuCommand('启用HTML5视频截图器',HTML5VideoCapturer); }else HTML5VideoCapturer();