/* ── Responsive: mobile portrait ── */ @media (max-width: 600px) and (orientation: portrait) { body { overflow-y: auto !important; height: auto !important; } #app { grid-template-columns: 1fr !important; grid-template-rows: 38px auto auto 44px 22px !important; height: auto !important; overflow: visible !important; } #panel-left { grid-column: 1; grid-row: 2; height: 80px; flex-direction: row; } #decks-area { grid-column: 1; grid-row: 3; grid-template-columns: 1fr 1fr !important; height: 300px; } #panel-right { display: none; } #xf-section { grid-column: 1; grid-row: 4; } #outs-bar { grid-column: 1; grid-row: 5; } } /* ── Keep landscape (mobile + desktop) always showing the grid ── */ @media (orientation: landscape), (min-width: 601px) { body { overflow: hidden !important; height: 100dvh !important; } #app { display: grid !important; grid-template-rows: 38px 1fr 44px 22px !important; grid-template-columns: 52px 1fr 52px !important; height: 100dvh !important; overflow: hidden !important; } #panel-left { grid-column: 1 !important; grid-row: 2 !important; display: flex !important; } #decks-area { grid-column: 2 !important; grid-row: 2 !important; } #panel-right { grid-column: 3 !important; grid-row: 2 !important; display: flex !important; } #xf-section { grid-column: 1/-1 !important; grid-row: 3 !important; } #outs-bar { grid-column: 1/-1 !important; grid-row: 4 !important; } } /* ── Tap targets ── */ .src-btn { min-height: 18px; } .d-play { min-width: 24px; min-height: 24px; } .fxbtn { min-height: 28px; } .mpill { min-height: 26px; min-width: 40px; } .tb-btn { min-height: 38px; }
SISTEMA AV PROFESIONAL EN VIVO
INICIAR SESIÓN
Necesitás una cuenta Google para acceder.
Acceso con cuenta Google · VJMIX PRO v3.0
VJMIXPRO
SISTEMA AV EN VIVO
PRO
SIN SESIÓN
— BPM
VIS
PARTÍCULAS
MASTER
SIN FUENTE
A
SIN FUENTE
+
DOBLE CLIC
CÁMARA
Sin cámaras
SIN FUENTE
B
SIN FUENTE
+
DOBLE CLIC
CÁMARA
Sin cámaras
SIN FUENTE
C
SIN FUENTE
+
DOBLE CLIC
CÁMARA
Sin cámaras
DRONE
D
DRONE
+
DRONE / STREAM
CÁMARA
Sin cámaras
A
DECK A
50%
B
DECK B
ZONA 1
A
ZONA 2
B
MASTER
MIX
ZONA 4
D
STREAM
NET
HUE ROT
SATURAR100%
BRILLO100%
CONTRASTE100%
INTENSIDAD50
DECK A
DECK B
DECK C
DECK D
Bridge desconectado
CH1
0
CH2
0
CH3
0
CH4
0
CH5
0
CH6
0
CH7
0
CH8
0
Tocá LANZAR en la barra inferior. Arrastrá la ventana a la pantalla → F11.
DISPOSITIVOS WLED
Sin dispositivos
BRILLO128
ESCENAS
VELOCIDAD
ZOOM
DENSIDAD50%
COLOR HUE200°
MIDI CONECTADO
MAPEAR:
`);w.document.close();setTimeout(()=>{const oc=w.document.getElementById('oc');if(!oc)return;oc.width=w.innerWidth||1920;oc.height=w.innerHeight||1080;(function push(){if(!w||w.closed){o.active=false;updOut(z);return;}w.requestAnimationFrame(push);try{const ot=oc.getContext('2d');if(o.deck==='master')ot.drawImage(canvas,0,0,oc.width,oc.height);else if(o.deck==='black'){ot.fillStyle='#000';ot.fillRect(0,0,oc.width,oc.height);}else{const v=V[o.deck];if(v&&v.readyState>=2){ot.filter=buildFilter();ot.drawImage(v,0,0,oc.width,oc.height);}}}catch(_){}})();},300);o.win=w;o.active=true;updOut(z);notify('Zona '+z+' activa');} function updOut(z){const el=document.getElementById('os'+z);if(!el)return;el.classList.toggle('live',S.outputs[z].active);document.getElementById('mhb-live').style.display=Object.values(S.outputs).some(o=>o.active)?'block':'none';} let curOut=1; function openOutModal(z){curOut=z;document.getElementById('out-zone-lbl').textContent=z;const sel=S.outputs[z].deck;document.querySelectorAll('.odcard').forEach(d=>d.classList.toggle('sel',d.dataset.d===sel));openModal('modal-out');} function assignOut(d,btn){S.outputs[curOut].deck=d;document.querySelectorAll('.odcard').forEach(b=>b.classList.remove('sel'));btn.classList.add('sel');document.querySelector('#os'+curOut+' .odeck').textContent=d.toUpperCase();} // ── MULTI-CAMERA SYSTEM ──────────────────────────────── // Clave: cada deck llama getUserMedia con deviceId específico // Múltiples streams coexisten en paralelo sin conflicto async function enumCams(){ // Primero pedir permiso genérico para poder ver los labels try{ const tmp=await navigator.mediaDevices.getUserMedia({video:true,audio:false}); tmp.getTracks().forEach(t=>t.stop()); }catch(e){} try{ const devices=await navigator.mediaDevices.enumerateDevices(); S.cameras=devices.filter(d=>d.kind==='videoinput'); notify('Cámaras detectadas: '+S.cameras.length); // Actualizar listas en todos los decks ['a','b','c','d'].forEach(id=>renderCamList(id)); return S.cameras; }catch(e){ notify('Error enumerando cámaras: '+e.message); return[]; } } function renderCamList(deckId){ const container=document.getElementById('camlist-'+deckId); if(!container)return; if(!S.cameras.length){ container.innerHTML='
Sin cámaras · Tocá 📷 CÁMARAS arriba
'; return; } container.innerHTML=''; S.cameras.forEach((cam,i)=>{ const label=cam.label||('Cámara '+(i+1)); const isActive=S.decks[deckId].deviceId===cam.deviceId; const div=document.createElement('div'); div.className='cam-item'+(isActive?' active':''); div.innerHTML=`
${label.substring(0,32)}`; div.onclick=(e)=>{e.stopPropagation();assignCam(deckId,cam.deviceId,label);closeCamPick(deckId);}; container.appendChild(div); }); } async function assignCam(deckId,deviceId,label){ // Detener stream anterior de este deck clrStr(deckId); try{ const constraints={ video:{ deviceId:{exact:deviceId}, width:{ideal:1920}, height:{ideal:1080}, frameRate:{ideal:60} }, audio:false }; const stream=await navigator.mediaDevices.getUserMedia(constraints); S.decks[deckId].stream=stream; S.decks[deckId].srcType='cam'; S.decks[deckId].deviceId=deviceId; V[deckId].srcObject=stream; V[deckId].src=''; V[deckId].play().catch(()=>{}); S.decks[deckId].playing=true; document.getElementById('play-'+deckId).textContent='⏸'; document.getElementById('deck-'+deckId).classList.add('playing','has-cam'); document.getElementById('lbl-'+deckId).textContent=label.substring(0,16); document.getElementById('add-'+deckId).style.display='none'; document.getElementById('cbadge-'+deckId).textContent=label.substring(0,10); // Mark active source button setActiveSrcBtn(deckId,'cam'); notify('📷 '+label.substring(0,20)+' → Deck '+deckId.toUpperCase()); renderCamList(deckId); // refresh to show active }catch(e){ notify('Error cámara: '+e.message); } } function toggleCamPick(deckId){ // Cerrar todos los otros ['a','b','c','d'].forEach(id=>{ if(id!==deckId)closeCamPick(id); }); const pick=document.getElementById('campick-'+deckId); if(pick.classList.contains('open')){ closeCamPick(deckId); }else{ // Enumerar cámaras si no lo hicimos aún if(!S.cameras.length){ enumCams().then(()=>renderCamList(deckId)); }else{ renderCamList(deckId); } pick.classList.add('open'); } } function closeCamPick(deckId){document.getElementById('campick-'+deckId).classList.remove('open');} // Cerrar cam pickers al hacer click fuera document.addEventListener('click',()=>{['a','b','c','d'].forEach(id=>closeCamPick(id));}); // ── Screen capture (independiente por deck) ──────────── async function srcScreen(deckId){ try{ const st=await navigator.mediaDevices.getDisplayMedia({video:{frameRate:{ideal:60}},audio:false}); clrStr(deckId); S.decks[deckId].stream=st;S.decks[deckId].srcType='screen'; V[deckId].srcObject=st;V[deckId].src=''; V[deckId].play().catch(()=>{}); S.decks[deckId].playing=true; document.getElementById('play-'+deckId).textContent='⏸'; document.getElementById('deck-'+deckId).classList.add('playing'); document.getElementById('deck-'+deckId).classList.remove('has-cam'); document.getElementById('lbl-'+deckId).textContent='Pantalla'; document.getElementById('add-'+deckId).style.display='none'; setActiveSrcBtn(deckId,'scr'); notify('🖥 Pantalla → Deck '+deckId.toUpperCase()); // Auto-stop on track end st.getTracks()[0].onended=()=>{clrStr(deckId);}; }catch(e){if(e.name!=='NotAllowedError')notify('Error: '+e.message);} } // ── File source ──────────────────────────────────────── let csd='a'; function setSrcDeck(id){csd=id;} function srcFile(){document.getElementById('hfile').click();} function onFile(evt){ const f=evt.target.files[0];if(!f)return; const url=URL.createObjectURL(f); clrStr(csd); V[csd].src=url;V[csd].srcObject=null;V[csd].loop=true;V[csd].load(); V[csd].play().catch(()=>{}); S.decks[csd].playing=true;S.decks[csd].srcType='file'; document.getElementById('play-'+csd).textContent='⏸'; document.getElementById('deck-'+csd).classList.add('playing'); document.getElementById('deck-'+csd).classList.remove('has-cam'); document.getElementById('lbl-'+csd).textContent=f.name.substring(0,16); document.getElementById('add-'+csd).style.display='none'; setActiveSrcBtn(csd,'file'); closeModal('modal-src'); notify('📁 '+f.name.substring(0,20)+' → Deck '+csd.toUpperCase()); evt.target.value=''; } // ── URL / Stream source ──────────────────────────────── function openSrcUrl(deckId){csd=deckId;document.getElementById('src-deck-lbl').textContent=deckId.toUpperCase();document.getElementById('url-field').style.display='flex';document.getElementById('modal-cam-list').style.display='none';document.getElementById('url-inp').value='';openModal('modal-src');} function srcUrl(){ const url=document.getElementById('url-inp').value.trim();if(!url)return; clrStr(csd); V[csd].srcObject=null; // Use hls.js for .m3u8 on non-Safari browsers if(url.includes('.m3u8') && typeof Hls !== 'undefined' && Hls.isSupported()){ const hls=new Hls({lowLatencyMode:true,backBufferLength:5}); hls.loadSource(url); hls.attachMedia(V[csd]); hls.on(Hls.Events.MANIFEST_PARSED,()=>V[csd].play().catch(()=>{})); V[csd]._hls=hls; // save ref for cleanup } else { V[csd].src=url;V[csd].load();V[csd].play().catch(()=>{}); } S.decks[csd].playing=true;S.decks[csd].srcType='url'; document.getElementById('play-'+csd).textContent='⏸'; document.getElementById('deck-'+csd).classList.add('playing'); document.getElementById('deck-'+csd).classList.remove('has-cam'); document.getElementById('lbl-'+csd).textContent=url.substring(0,18); document.getElementById('add-'+csd).style.display='none'; setActiveSrcBtn(csd,'url'); closeModal('modal-src'); notify('📡 Stream → Deck '+csd.toUpperCase()); } // ── Source modal helpers ─────────────────────────────── function openSrc(id){ csd=id; document.getElementById('src-deck-lbl').textContent=id.toUpperCase(); document.getElementById('url-field').style.display='none'; document.getElementById('modal-cam-list').style.display='none'; openModal('modal-src'); } function showUrlF(){document.getElementById('url-field').style.display='flex';} function setHint(url){document.getElementById('url-inp').value=url;document.getElementById('url-field').style.display='flex';} function srcCamModal(){ // Show camera list in modal if(!S.cameras.length){ enumCams().then(()=>renderModalCamList()); }else{ renderModalCamList(); } document.getElementById('modal-cam-list').style.display='block'; } function renderModalCamList(){ const container=document.getElementById('modal-cam-items'); container.innerHTML=''; if(!S.cameras.length){ container.innerHTML='
Sin cámaras detectadas. Conectá una cámara y tocá 📷 CÁMARAS en la barra superior.
'; return; } S.cameras.forEach((cam,i)=>{ const label=cam.label||('Cámara '+(i+1)); const div=document.createElement('div'); div.className='cam-item'; div.innerHTML=`
${label.substring(0,40)}`; div.onclick=()=>{assignCam(csd,cam.deviceId,label);closeModal('modal-src');}; container.appendChild(div); }); } // ── Active source button highlight ──────────────────── function setActiveSrcBtn(deckId,type){ ['cam','scr','file','url'].forEach(t=>{ const btn=document.getElementById('stype-'+deckId+'-'+t); if(btn)btn.classList.toggle('active-src',t===type); }); } // ── Deck transport ───────────────────────────────────── function togglePlay(id){ const v=V[id],d=S.decks[id]; if(!v.src&&!v.srcObject){openSrc(id);return;} if(d.playing){ v.pause();d.playing=false; document.getElementById('play-'+id).textContent='▶'; document.getElementById('deck-'+id).classList.remove('playing'); updateDeckState(id,'paused'); }else{ v.play().catch(()=>{});d.playing=true; document.getElementById('play-'+id).textContent='⏸'; document.getElementById('deck-'+id).classList.add('playing'); updateDeckState(id,'playing'); logEvent('Deck '+id.toUpperCase()+' ▶ playing','ok'); } } function deckClick(id){ document.querySelectorAll('.deck').forEach(d=>d.classList.remove('sel')); document.getElementById('deck-'+id).classList.add('sel'); } function setSpd(id,v,vid){S.decks[id].speed=parseFloat(v);V[id].playbackRate=parseFloat(v);document.getElementById(vid).textContent=parseFloat(v).toFixed(2)+'×';} function revDecks(){['a','b','c','d'].forEach(id=>{S.decks[id].speed*=-1;V[id].playbackRate=Math.abs(S.decks[id].speed);});notify('Reverso activado');} function resetSpd(){['a','b','c','d'].forEach(id=>{S.decks[id].speed=1;V[id].playbackRate=1;});['a','b','c','d'].forEach(id=>{const e=document.getElementById('sv-'+id);if(e)e.textContent='1×';});} function clrStr(id){ if(V[id]._hls){V[id]._hls.destroy();V[id]._hls=null;} if(S.decks[id].stream){S.decks[id].stream.getTracks().forEach(t=>t.stop());S.decks[id].stream=null;} S.decks[id].deviceId=null; document.getElementById('deck-'+id).classList.remove('has-cam'); } // ── XF & Mode ────────────────────────────────────────── function updateXF(v){S.xf=parseFloat(v);document.getElementById('xf-pct').textContent=Math.round(v)+'%';} function setMode(m,btn){S.mode=m;document.querySelectorAll('.mpill').forEach(b=>b.classList.remove('on'));btn.classList.add('on');document.getElementById('mhb-mode').textContent=m.toUpperCase();} // ── FX ───────────────────────────────────────────────── function sfx(k,v,vid){S.fx[k]=parseFloat(v);const el=document.getElementById(vid);if(el){if(k==='hue')el.textContent=Math.round(v)+'°';else if(k==='amt')el.textContent=Math.round(v);else el.textContent=Math.round(v)+'%';}} function tFX(n,btn){S.fxOn[n]=!S.fxOn[n];btn.classList.toggle('on',S.fxOn[n]);} function setBlend(m,btn){S.blend=m;document.querySelectorAll('#fxp-blend .fxbtn').forEach(b=>b.classList.remove('on'));btn.classList.add('on');} function switchFX(tab,btn){document.querySelectorAll('.fxtab').forEach(t=>t.classList.remove('on'));btn.classList.add('on');document.querySelectorAll('.fxp').forEach(p=>p.classList.remove('on'));document.getElementById('fxp-'+tab).classList.add('on');} // ── DMX / Madrix ─────────────────────────────────────── const dmxSend=(()=>{let t=null;return()=>{clearTimeout(t);t=setTimeout(()=>wsSend({type:'dmx_send',universe:S.dmx.universe,channels:S.dmx.channels}),16);};})(); function dmxCh(ch,val){S.dmx.channels[ch-1]=parseInt(val);document.getElementById('chv'+ch).textContent=val;dmxSend();} function blackout(){S.dmx.channels.fill(0);document.querySelectorAll('.chsl').forEach(s=>{s.value=0;});for(let i=1;i<=8;i++){const e=document.getElementById('chv'+i);if(e)e.textContent='0';}dmxSend();notify('BLACKOUT DMX');} function saveDmxCfg(){S.dmx.ip=document.getElementById('dmx-ip').value.trim();S.dmx.universe=parseInt(document.getElementById('dmx-univ').value)||0;wsSend({type:'artnet_config',data:{ip:S.dmx.ip,universe:S.dmx.universe,port:6454}});notify('Madrix: '+S.dmx.ip);} function saveDmxScene(){const name=document.getElementById('scene-name').value.trim();if(!name)return;S.dmxScenes[name]=[...S.dmx.channels];wsSend({type:'dmx_scene_save',name,channels:[...S.dmx.channels],universe:S.dmx.universe});saveScL();renderSc();document.getElementById('scene-name').value='';notify('Escena: '+name);} function fireSc(name){S.dmx.channels=[...(S.dmxScenes[name]||[])];S.dmx.channels.forEach((v,i)=>{if(i<8){const s=document.querySelectorAll('.chsl')[i];if(s)s.value=v;const cv=document.getElementById('chv'+(i+1));if(cv)cv.textContent=v;}});wsSend({type:'dmx_scene_fire',name});notify('▶ '+name);} function delSc(name,e){e.stopPropagation();delete S.dmxScenes[name];saveScL();renderSc();} function renderSc(){const el=document.getElementById('dmx-scenes');el.innerHTML='';Object.keys(S.dmxScenes).forEach(name=>{const c=document.createElement('div');c.className='sch';c.innerHTML=`▶ ${name}`;el.appendChild(c);});} function saveScL(){try{localStorage.setItem('vjmix_sc',JSON.stringify(S.dmxScenes));}catch(_){}} function loadScL(){try{S.dmxScenes=JSON.parse(localStorage.getItem('vjmix_sc')||'{}');renderSc();}catch(_){}} // ── BPM ──────────────────────────────────────────────── document.getElementById('btn-tap').addEventListener('click',()=>{ const now=Date.now(),bpm=S.bpm; if(now-bpm.last>3000)bpm.taps=[]; bpm.last=now;bpm.taps.push(now);if(bpm.taps.length>8)bpm.taps.shift(); if(bpm.taps.length>=2){const avg=(bpm.taps[bpm.taps.length-1]-bpm.taps[0])/(bpm.taps.length-1);bpm.val=Math.round(60000/avg);document.getElementById('bpm-num').textContent=bpm.val+' BPM';if(bpm.int)clearInterval(bpm.int);const dot=document.getElementById('beat-dot');bpm.int=setInterval(()=>{dot.classList.add('f');setTimeout(()=>dot.classList.remove('f'),75);},avg);} }); // ── Recording ────────────────────────────────────────── function toggleRec(){const btn=document.getElementById('btn-rec');if(S.rec){S.recorder.stop();S.rec=false;btn.classList.remove('rec');btn.innerHTML='REC';}else{const st=canvas.captureStream(60);const chunks=[];let mt='video/webm;codecs=vp9';if(!MediaRecorder.isTypeSupported(mt))mt='video/webm';S.recorder=new MediaRecorder(st,{mimeType:mt});S.recorder.ondataavailable=e=>chunks.push(e.data);S.recorder.onstop=()=>{const blob=new Blob(chunks,{type:'video/webm'});const u=URL.createObjectURL(blob);const a=document.createElement('a');a.href=u;a.download='vjmix_'+Date.now()+'.webm';a.click();notify('Grabación guardada');};S.recorder.start(100);S.rec=true;btn.classList.add('rec');btn.innerHTML='STOP';notify('● Grabando master output...');}} // ── Presets ──────────────────────────────────────────── function savePreset(){const name=prompt('Nombre del preset:','Escena '+(Date.now()%10000));if(!name)return;const p={name,xf:S.xf,mode:S.mode,fx:{...S.fx},fxOn:{...S.fxOn},blend:S.blend,ts:Date.now()};const ps=JSON.parse(localStorage.getItem('vjmix_presets')||'[]');ps.push(p);localStorage.setItem('vjmix_presets',JSON.stringify(ps.slice(-20)));notify('Preset: '+name);} // ── WebSocket ────────────────────────────────────────── function connectSess(){const wu=document.getElementById('worker-url').value.trim();const id=document.getElementById('sess-id-inp').value.trim().toUpperCase();if(!wu||!id){notify('Completá URL y ID');return;}localStorage.setItem('vjmix_wu',wu);S.sessionId=id;const wsUrl=wu.replace(/\/api.*/,'')+'/api/session/'+id+'?role=vj&label=VJ-Controller';document.getElementById('sess-status').textContent='Conectando...';try{S.ws=new WebSocket(wsUrl);S.ws.onopen=()=>{document.getElementById('sdot').classList.add('on');document.getElementById('sess-lbl').textContent=id;document.getElementById('sess-status').textContent='Conectado ✓';closeModal('modal-sess');notify('Sesión '+id+' activa');};S.ws.onmessage=(e)=>{try{handleWS(JSON.parse(e.data));}catch(_){}};S.ws.onclose=()=>{document.getElementById('sdot').classList.remove('on');};S.ws.onerror=()=>{document.getElementById('sess-status').textContent='Error de conexión';};}catch(e){document.getElementById('sess-status').textContent='Error: '+e.message;}} function handleWS(msg){ // Route WebRTC signaling to QR camera system if(msg.type==='rtc_offer'||msg.type==='rtc_answer'||(msg.type==='rtc_ice'&&msg.deck)){ handleWebRTCSignal(msg); return; } if(msg.type==='peer_join'&&msg.role==='bridge'){document.getElementById('dmx-status').textContent='Bridge conectado — ArtNet activo ✓';document.getElementById('dmx-status').style.color='var(--ag)';document.getElementById('mhb-madrix').style.display='block';notify('ArtNet Bridge → Madrix conectado');}if(msg.type==='peer_leave'&&msg.role==='bridge'){document.getElementById('dmx-status').textContent='Bridge desconectado';document.getElementById('dmx-status').style.color='var(--t3)';document.getElementById('mhb-madrix').style.display='none';}} function wsSend(msg){if(S.ws&&S.ws.readyState===1)S.ws.send(JSON.stringify(msg));} // ── Modals ───────────────────────────────────────────── function openModal(id){document.getElementById(id).classList.add('open');} function closeModal(id){document.getElementById(id).classList.remove('open');} // ── Keyboard ─────────────────────────────────────────── document.addEventListener('keydown',e=>{ // Close FX panel on ESC if(e.key==='Escape'){ const panel=document.getElementById('fx-content-panel'); if(panel&&panel.classList.contains('open')){ panel.classList.remove('open');_fxPanelOpen=false;return; } } if(e.target.tagName==='INPUT')return; const k=e.key.toLowerCase(); if(k==='a')togglePlay('a');if(k==='b')togglePlay('b'); if(k==='c')togglePlay('c');if(k==='d')togglePlay('d'); if(k==='t')document.getElementById('btn-tap').click(); if(k==='r')toggleRec(); if(k==='arrowleft'){S.xf=Math.max(0,S.xf-5);document.getElementById('xfader').value=S.xf;updateXF(S.xf);} if(k==='arrowright'){S.xf=Math.min(100,S.xf+5);document.getElementById('xfader').value=S.xf;updateXF(S.xf);} if(k==='1')launchOutput(1);if(k==='2')launchOutput(2); if(k==='3')launchOutput(3);if(k==='4')launchOutput(4); if(k==='escape')document.querySelectorAll('.modal.open').forEach(m=>m.classList.remove('open')); }); // ── Notifications ────────────────────────────────────── function notify(msg,type){ const a=document.getElementById('notifs'); if(!a)return; const el=document.createElement('div'); el.className='notif';el.textContent=msg; a.appendChild(el);setTimeout(()=>el.remove(),3200); } // ── PWA ──────────────────────────────────────────────── // PWA — manifest.json y sw.js son archivos reales en /public if('serviceWorker' in navigator){ navigator.serviceWorker.register('/sw.js').then(reg => { // Check for updates every time app opens reg.update(); // When new version available, activate immediately reg.addEventListener('updatefound', () => { const newSW = reg.installing; newSW.addEventListener('statechange', () => { if(newSW.state === 'installed' && navigator.serviceWorker.controller){ // New version ready - reload to apply newSW.postMessage('skipWaiting'); notify('✓ App actualizada'); setTimeout(() => window.location.reload(), 1500); } }); }); }).catch(e => console.warn('[SW] error:', e)); // Reload when SW takes control let refreshing = false; navigator.serviceWorker.addEventListener('controllerchange', () => { if(!refreshing){ refreshing = true; window.location.reload(); } }); } // ── Boot ─────────────────────────────────────────────── // ═══════════════════════════════════════════════════════ // PRO LAYOUT — FX Panel toggle + Awareness System // ═══════════════════════════════════════════════════════ let _fxPanelOpen = false; let _fxCurrentTab = 'color'; function switchFXPanel(tab, btn) { const panel = document.getElementById('fx-content-panel'); if (!panel) return; // If clicking same tab while open → close if (_fxPanelOpen && _fxCurrentTab === tab) { panel.classList.remove('open'); _fxPanelOpen = false; document.querySelectorAll('.fxtab-v').forEach(b => b.classList.remove('on')); return; } // Open panel and switch tab panel.classList.add('open'); _fxPanelOpen = true; _fxCurrentTab = tab; // Update vertical tab buttons document.querySelectorAll('.fxtab-v').forEach(b => b.classList.remove('on')); const vtab = document.getElementById('ftv-' + tab); if (vtab) vtab.classList.add('on'); // Update horizontal tab buttons inside panel document.querySelectorAll('.fxtab-h').forEach(b => b.classList.remove('on')); const htab = document.querySelector(`.fxtab-h[onclick*="'${tab}'"]`); if (htab) htab.classList.add('on'); // Show correct fx panel document.querySelectorAll('.fxp').forEach(p => p.classList.remove('on')); const fp = document.getElementById('fxp-' + tab); if (fp) fp.classList.add('on'); // Init vis if needed if (tab === 'vis' && !VIS.running) visInit(); if (tab === 'wled') wledInit(); } // Close FX panel on outside tap document.addEventListener('click', e => { const panel = document.getElementById('fx-content-panel'); const right = document.getElementById('panel-right'); if (_fxPanelOpen && panel && !panel.contains(e.target) && !right.contains(e.target)) { panel.classList.remove('open'); _fxPanelOpen = false; } }); // Keep old switchFX working (called from modals) function switchFX(tab, btn) { switchFXPanel(tab, btn); } // ── AWARENESS SYSTEM ──────────────────────────────────── const LOG = []; function logEvent(msg, type = '') { const now = new Date(); const time = now.getHours().toString().padStart(2,'0') + ':' + now.getMinutes().toString().padStart(2,'0'); LOG.unshift({ time, msg, type }); if (LOG.length > 20) LOG.pop(); renderLog(); // Also show as notify for important events if (type === 'live' || type === 'warn') notify(msg); } function renderLog() { const el = document.getElementById('log-strip'); if (!el) return; if (LOG.length === 0) { el.classList.remove('visible'); return; } el.classList.add('visible'); el.innerHTML = LOG.slice(0,5).map(e => `
${e.time}${e.msg}
` ).join(''); } // ── DECK STATE BADGES ─────────────────────────────────── function updateDeckState(id, state, srcName) { const badge = document.getElementById('state-' + id); if (!badge) return; badge.className = 'deck-state'; if (state === 'playing') { badge.className += ' state-playing'; badge.textContent = '▶ PLAYING'; } else if (state === 'paused') { badge.className += ' state-paused'; badge.textContent = '⏸ PAUSED'; } else if (state === 'buffering') { badge.className += ' state-buffering'; badge.textContent = '⟳ BUFFERING'; } else { badge.className += ' state-nosrc'; badge.textContent = srcName || 'SIN FUENTE'; } } // ── VIS MINI CANVAS (left panel) ──────────────────────── // Override visInit to also use the left panel canvas const _origVisInit = window.visInit; function visInitMini() { // Use vis-canvas in left panel as the primary vis canvas const miniCanvas = document.getElementById('vis-canvas'); if (miniCanvas) { // Sync vis output to mini canvas VIS.canvas = miniCanvas; } if (_origVisInit) _origVisInit(); else { VIS.canvas = document.getElementById('vis-canvas'); if (!VIS.canvas) return; VIS.gl = VIS.canvas.getContext('webgl', {antialias:false,powerPreference:'high-performance'}) || VIS.canvas.getContext('experimental-webgl'); if (!VIS.gl) VIS.ctx2d = VIS.canvas.getContext('2d'); resizeVisCanvas(); VIS.startTime = performance.now(); VIS.running = true; visLoadScene('particles', document.querySelector('[data-scene="particles"]')); } } // ── FLIP CAMERA — alterna frontal/trasera en el deck activo ── const DECK_FACING = { a:'environment', b:'environment', c:'environment', d:'environment' }; async function flipDeckCamera(deckId) { // Toggle facing mode DECK_FACING[deckId] = DECK_FACING[deckId] === 'environment' ? 'user' : 'environment'; const facing = DECK_FACING[deckId]; // Stop current stream clrStr(deckId); try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: facing }, width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }, audio: false }); S.decks[deckId].stream = stream; S.decks[deckId].srcType = 'cam'; V[deckId].srcObject = stream; V[deckId].play().catch(() => {}); S.decks[deckId].playing = true; document.getElementById('play-' + deckId).textContent = '⏸'; document.getElementById('deck-' + deckId).classList.add('playing', 'has-cam'); document.getElementById('add-' + deckId).style.display = 'none'; setActiveSrcBtn(deckId, 'cam'); const label = facing === 'user' ? '📱 Frontal' : '📷 Trasera'; document.getElementById('lbl-' + deckId).textContent = label; // Update flip button icon const btn = document.getElementById('flip-' + deckId); if (btn) btn.textContent = facing === 'user' ? '🤳' : '🔄'; notify(label + ' → Deck ' + deckId.toUpperCase()); const ob = document.getElementById('flip-overlay-' + deckId); if(ob) { ob.style.display = 'flex'; ob.textContent = facing === 'user' ? '🤳' : '🔄'; } } catch(e) { notify('Error cámara: ' + e.message); DECK_FACING[deckId] = DECK_FACING[deckId] === 'user' ? 'environment' : 'user'; } } // Quick cam — abre cámara trasera directo sin picker async function quickCam(deckId) { clrStr(deckId); const facing = DECK_FACING[deckId] || 'environment'; try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: facing }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }); S.decks[deckId].stream = stream; S.decks[deckId].srcType = 'cam'; V[deckId].srcObject = stream; V[deckId].play().catch(() => {}); S.decks[deckId].playing = true; document.getElementById('play-' + deckId).textContent = '⏸'; document.getElementById('deck-' + deckId).classList.add('playing', 'has-cam'); document.getElementById('add-' + deckId).style.display = 'none'; document.getElementById('lbl-' + deckId).textContent = facing === 'user' ? '📱 Frontal' : '📷 Trasera'; setActiveSrcBtn(deckId, 'cam'); notify('📷 Cámara → Deck ' + deckId.toUpperCase()); // Show overlay flip button const ob = document.getElementById('flip-overlay-' + deckId); if(ob) ob.style.display = 'flex'; } catch(e) { notify('Error: ' + e.message); } } window.addEventListener('load',()=>{ setTimeout(()=>{document.getElementById('loading').classList.add('gone');setTimeout(()=>{const l=document.getElementById('loading');if(l)l.remove();},600);},1400); resizeC();loadScL(); // Auto-enum cameras on load enumCams(); // Restore worker URL // Auto-detect local server vs Cloudflare const isLocal = location.hostname !== 'minera-a4106.web.app' && !location.hostname.includes('pages.dev') && !location.hostname.includes('workers.dev'); const localWS = isLocal ? `ws://${location.hostname}:${location.port||3000}/api/session/` : null; const wu = localStorage.getItem('vjmix_wu') || localWS || 'wss://vj-mr-softwares.lechuteo.workers.dev'; document.getElementById('worker-url').value = wu; if (isLocal) { notify('Modo local · Servidor: ' + location.host); document.getElementById('worker-url').value = wu.replace('/api/session/',''); } notify('VJMIX PRO listo · Cámaras enumeradas automáticamente'); }); window.addEventListener('orientationchange',()=>setTimeout(resizeC,400)); `); w.document.close(); VIS.outputWindow = w; setTimeout(() => { const oc = w.document.getElementById('oc'); if (!oc) return; oc.width = w.innerWidth || 1920; oc.height = w.innerHeight || 1080; (function push() { if (!w || w.closed) { VIS.outputWindow = null; return; } w.requestAnimationFrame(push); try { const octx = oc.getContext('2d'); octx.drawImage(VIS.canvas, 0, 0, oc.width, oc.height); } catch(_) {} })(); }, 300); notify('✦ Visuales → Salida fullscreen'); } function visFullscreen() { if (VIS.canvas) { if (VIS.canvas.requestFullscreen) VIS.canvas.requestFullscreen(); else if (VIS.canvas.webkitRequestFullscreen) VIS.canvas.webkitRequestFullscreen(); } } // ── Init on load ────────────────────────────────────────── window.addEventListener('load', () => { setTimeout(visInit, 1800); window.addEventListener('resize', resizeVisCanvas); }); // Auto-init when tab is opened document.addEventListener('DOMContentLoaded', () => { const observer = new MutationObserver(() => { const panel = document.getElementById('fxp-vis'); if (panel && panel.classList.contains('on') && !VIS.running) { visInit(); } }); const fxBody = document.getElementById('fxbody'); if (fxBody) observer.observe(fxBody, { subtree: true, attributes: true, attributeFilter: ['class'] }); });