Skip to content

Instantly share code, notes, and snippets.

@Dhyfer1
Created August 13, 2026 12:46
Show Gist options
  • Select an option

  • Save Dhyfer1/41af5fd024471b71661fe7bd5aaab945 to your computer and use it in GitHub Desktop.

Select an option

Save Dhyfer1/41af5fd024471b71661fe7bd5aaab945 to your computer and use it in GitHub Desktop.
(()=>{
'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