Created
August 13, 2026 12:46
-
-
Save Dhyfer1/41af5fd024471b71661fe7bd5aaab945 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (()=>{ | |
| 'use strict'; | |
| const VERSION='3.5.0'; | |
| window.__VideoToolsKiwiLoaded=VERSION; | |
| const fmt=t=>{if(!Number.isFinite(t)||t<0)return'0:00';t=Math.floor(t);const h=Math.floor(t/3600),m=Math.floor(t%3600/60),s=t%60;return h?`${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`:`${m}:${String(s).padStart(2,'0')}`}; | |
| function notice(text,good=true,ms=2200){const n=document.createElement('div');n.className='kvt-notice';n.dataset.good=good?'1':'0';n.textContent=text;document.documentElement.appendChild(n);setTimeout(()=>n.remove(),ms)} | |
| let activeVideo=null, ui=null, hit=null, track=null, fill=null, thumb=null, timeEl=null, shot=null; | |
| let dragging=false, wasPlaying=false, keepPaused=false, pauseUntil=0, raf=0, layoutTimer=0; | |
| let boundVideo=null; | |
| let uiHost=null; | |
| function uiContainer(){ | |
| const fs=document.fullscreenElement; | |
| return (fs && fs.contains(document.documentElement)===false) ? fs : (fs || document.documentElement); | |
| } | |
| function placeUIHost(){ | |
| if(!ui || !ui.isConnected)return; | |
| const host=document.fullscreenElement || document.documentElement; | |
| if(ui.parentElement!==host){ | |
| host.appendChild(ui); | |
| uiHost=host; | |
| } | |
| } | |
| function ensureUI(){ | |
| if(ui?.isConnected)return; | |
| ui=document.createElement('div'); | |
| ui.className='kvt-ui'; | |
| ui.innerHTML='<div class="kvt-hit"><div class="kvt-track"><div class="kvt-fill"></div><div class="kvt-thumb"></div></div><span class="kvt-time">0:00 / 0:00</span><button class="kvt-shot" type="button" title="Capturar frame" aria-label="Capturar frame">đź“·</button></div>'; | |
| document.documentElement.appendChild(ui); | |
| uiHost=document.documentElement; | |
| hit=ui.querySelector('.kvt-hit'); track=ui.querySelector('.kvt-track'); fill=ui.querySelector('.kvt-fill'); thumb=ui.querySelector('.kvt-thumb'); timeEl=ui.querySelector('.kvt-time'); shot=ui.querySelector('.kvt-shot'); | |
| hit.addEventListener('pointerdown',begin); hit.addEventListener('pointermove',move); hit.addEventListener('pointerup',finish); hit.addEventListener('pointercancel',finish); | |
| shot.addEventListener('pointerdown',e=>e.stopPropagation()); | |
| shot.addEventListener('click',e=>{e.preventDefault();e.stopPropagation();if(activeVideo)captureVisibleFrame(activeVideo,ui)}); | |
| } | |
| function videos(){return [...document.querySelectorAll('video')];} | |
| function visibleScore(v){ | |
| if(!v||!v.isConnected)return -1; | |
| const r=v.getBoundingClientRect(), cs=getComputedStyle(v); | |
| if(cs.display==='none'||cs.visibility==='hidden'||parseFloat(cs.opacity||'1')===0)return -1; | |
| const w=r.width,h=r.height; | |
| if(w<80||h<45||r.right<=0||r.left>=innerWidth||r.bottom<=0||r.top>=innerHeight)return -1; | |
| const area=Math.max(0,Math.min(r.right,innerWidth)-Math.max(r.left,0))*Math.max(0,Math.min(r.bottom,innerHeight)-Math.max(r.top,0)); | |
| if(area<80*45)return -1; | |
| let score=area; | |
| if(!v.paused&&!v.ended)score*=3; | |
| if(v.readyState>=2)score*=1.2; | |
| return score; | |
| } | |
| function chooseVideo(){ | |
| let best=null,bs=-1; | |
| for(const v of videos()){const s=visibleScore(v);if(s>bs){bs=s;best=v}} | |
| if(best!==activeVideo){bindVideo(best)} | |
| return best; | |
| } | |
| function bindVideo(v){ | |
| if(boundVideo){ | |
| ['timeupdate','progress','durationchange','loadedmetadata','seeking','seeked','play','pause','emptied'].forEach(e=>boundVideo.removeEventListener(e,onVideoEvent)); | |
| } | |
| activeVideo=v||null; boundVideo=v||null; | |
| if(boundVideo){ | |
| ['timeupdate','progress','durationchange','loadedmetadata','seeking','seeked','play','pause','emptied'].forEach(e=>boundVideo.addEventListener(e,onVideoEvent,{passive:true})); | |
| } | |
| updateProgress(); layout(true); | |
| } | |
| function onVideoEvent(){updateProgress();layout();} | |
| function layout(force=false){ | |
| if(!ui)return; | |
| placeUIHost(); | |
| const v=activeVideo||chooseVideo(); | |
| if(!v){ui.classList.remove('kvt-visible');return} | |
| const r=v.getBoundingClientRect(); | |
| const visibleScoreNow=visibleScore(v); | |
| if(visibleScoreNow<0){ui.classList.remove('kvt-visible');return} | |
| // Use the visible video rectangle directly. This works for normal, landscape and fullscreen layouts. | |
| let width=Math.max(110,Math.min(r.width,innerWidth-4)); | |
| let left=Math.max(2,Math.min(r.left,innerWidth-width-2)); | |
| let top=Math.round(r.bottom-30); | |
| if(top<r.top+2)top=Math.round(r.top+2); | |
| if(top>innerHeight-30)top=Math.round(innerHeight-30); | |
| ui.style.left=`${Math.round(left)}px`; ui.style.top=`${top}px`; ui.style.width=`${Math.round(width)}px`; | |
| ui.classList.add('kvt-visible'); | |
| } | |
| function updateProgress(){ | |
| if(!timeEl||!track||!fill||!thumb)return; | |
| const v=activeVideo||chooseVideo(); if(!v)return; | |
| const d=v.duration,ct=v.currentTime; | |
| if(!Number.isFinite(d)||d<=0){ | |
| fill.style.setProperty('width','0%','important'); thumb.style.left='0%'; timeEl.textContent=`${fmt(ct)} / 0:00`; return; | |
| } | |
| const q=Math.max(0,Math.min(1,ct/d)); | |
| // Use actual width rather than a scale transform; this avoids Android/WebView transform repaint issues. | |
| fill.style.setProperty('width',`${q*100}%`,'important'); | |
| fill.style.transform='translateY(-50%)'; | |
| const rr=track.getBoundingClientRect(); | |
| const px=Math.max(0,Math.min(rr.width,q*rr.width)); | |
| thumb.style.left=`${px}px`; | |
| timeEl.textContent=`${fmt(ct)} / ${fmt(d)}`; | |
| } | |
| function setPosFromX(x){ | |
| const v=activeVideo;if(!v||!track)return; | |
| const d=v.duration;if(!Number.isFinite(d)||d<=0)return; | |
| const rr=track.getBoundingClientRect(),q=Math.max(0,Math.min(1,(x-rr.left)/Math.max(1,rr.width))); | |
| try{v.currentTime=q*d}catch{} | |
| updateProgress(); | |
| if(keepPaused){try{v.pause()}catch{};pauseUntil=Math.max(pauseUntil,performance.now()+700)} | |
| } | |
| function begin(e){ | |
| if(!activeVideo)return;e.preventDefault();e.stopPropagation();dragging=true; | |
| wasPlaying=!activeVideo.paused&&!activeVideo.ended;keepPaused=!wasPlaying;pauseUntil=keepPaused?performance.now()+1800:0; | |
| try{activeVideo.pause()}catch{};hit.setPointerCapture?.(e.pointerId);setPosFromX(e.clientX); | |
| } | |
| function move(e){if(!dragging)return;e.preventDefault();e.stopPropagation();setPosFromX(e.clientX)} | |
| function finish(e){ | |
| if(e){e.preventDefault();e.stopPropagation()} | |
| if(!dragging)return;dragging=false; | |
| try{if(hit.hasPointerCapture?.(e.pointerId))hit.releasePointerCapture?.(e.pointerId)}catch{} | |
| if(wasPlaying){keepPaused=false;const p=activeVideo?.play?.();if(p?.catch)p.catch(()=>{})} | |
| else{keepPaused=true;pauseUntil=performance.now()+1200;try{activeVideo?.pause()}catch{};[50,150,350,700,1100].forEach(ms=>setTimeout(()=>{if(keepPaused)try{activeVideo?.pause()}catch{}},ms))} | |
| } | |
| function tick(){ | |
| chooseVideo(); | |
| if(keepPaused&&activeVideo&&performance.now()<pauseUntil&&!activeVideo.paused)try{activeVideo.pause()}catch{} | |
| if(keepPaused&&performance.now()>=pauseUntil)keepPaused=false; | |
| if(!dragging)updateProgress(); | |
| layout(); | |
| raf=requestAnimationFrame(tick); | |
| } | |
| function setupObservers(){ | |
| const relayout=()=>{cancelAnimationFrame(layoutTimer);layoutTimer=requestAnimationFrame(()=>{chooseVideo();layout(true);updateProgress()})}; | |
| addEventListener('resize',relayout,{passive:true}); addEventListener('orientationchange',()=>setTimeout(relayout,80),{passive:true}); addEventListener('scroll',relayout,{passive:true}); | |
| document.addEventListener('fullscreenchange',()=>setTimeout(()=>{placeUIHost();relayout()},80)); | |
| window.visualViewport?.addEventListener('resize',relayout,{passive:true}); | |
| window.visualViewport?.addEventListener('scroll',relayout,{passive:true}); | |
| if(window.ResizeObserver){const ro=new ResizeObserver(relayout);ro.observe(document.documentElement);} | |
| new MutationObserver(()=>{ensureUI();chooseVideo();layout(true)}).observe(document.documentElement,{childList:true,subtree:true}); | |
| } | |
| async function captureVisibleFrame(video){ | |
| if(!video||video.readyState<2){notice('El vĂdeo todavĂa no está listo',false);return} | |
| const wasPaused=video.paused,currentTime=video.currentTime; | |
| try{ | |
| const vw=video.videoWidth,vh=video.videoHeight; | |
| if(!vw||!vh){notice('No se pudo obtener la resoluciĂłn del vĂdeo',false);return} | |
| const c=document.createElement('canvas');c.width=vw;c.height=vh; | |
| const ctx=c.getContext('2d',{alpha:false});if(!ctx){notice('Canvas no disponible',false);return} | |
| try{ctx.drawImage(video,0,0,vw,vh);ctx.getImageData(0,0,1,1)}catch(err){console.error('[Video Tools] direct frame failed',err);notice('No se pudo extraer directamente el frame del vĂdeo',false,3500);return} | |
| const blob=await new Promise(resolve=>c.toBlob(resolve,'image/png'));if(!blob){notice('No se pudo crear el PNG',false);return} | |
| const data=await blobToDataUrl(blob);const safe=String(Number.isFinite(currentTime)?currentTime.toFixed(3):'0').replace('.','_'); | |
| const filename=`Video Tools/frame-${vw}x${vh}-${safe}s.png`; | |
| const saved=await new Promise(resolve=>chrome.runtime.sendMessage({type:'kvt_download_data_url',dataUrl:data,filename},x=>resolve(chrome.runtime.lastError?{ok:false,code:'DOWNLOAD_EXTENSION_ERROR',error:chrome.runtime.lastError.message}:x))); | |
| if(!saved?.ok){notice(`No se pudo guardar: ${saved?.code||'DOWNLOAD_ERROR'}`,false,3500);return} | |
| notice(`Frame guardado · ${vw} × ${vh}`,true,2400); | |
| }catch(e){console.error('[Video Tools] capture failed',e);notice('No se pudo capturar el frame',false,3500)} | |
| finally{if(Math.abs(video.currentTime-currentTime)>0.05)try{video.currentTime=currentTime}catch{};if(wasPaused&&!video.paused)try{video.pause()}catch{};if(!wasPaused&&video.paused){const p=video.play();if(p?.catch)p.catch(()=>{})}} | |
| } | |
| function blobToDataUrl(blob){return new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(r.result);r.onerror=reject;r.readAsDataURL(blob)})} | |
| function start(){ensureUI();chooseVideo();setupObservers();cancelAnimationFrame(raf);raf=requestAnimationFrame(tick)} | |
| if(document.documentElement)start();else addEventListener('DOMContentLoaded',start,{once:true}); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment