/* MAIN_TEACHER_SYNCED_FINAL_PART1.js Final Teacher JS (Part 1) - Contains OS detection, chat helpers, draggable chat, mos runtime helpers. - Adds universal "pseudo fullscreen" (Option B selected): ALL devices will use pseudo fullscreen. - Adds strong iOS inline/play protection (playsinline + webkit-playsinline + x5-playsinline). - Overrides Element.prototype.requestFullscreen / webkitRequestFullscreen / mozRequestFullScreen / msRequestFullscreen so no code triggers native fullscreen (we instead toggle pseudo fullscreen). - IMPORTANT: Paste your original initializeVideoPlayer(fetchUrl) (Part 2) into the placeholder below. */ // ---------- Robust Combined Fullscreen (native + pseudo, with iframe fallback) ---------- const videoContainer = document.getElementById('videoContainer'); const vid = document.getElementById('my-video'); const button = document.getElementById('button'); // // --- OS detection (minimal) --- const _ua = (navigator && navigator.userAgent || '').toLowerCase(); const _isAndroid = _ua.includes('android'); const _isIOS = /iphone|ipad|ipod/.test(_ua); const _isWindows = _ua.includes('windows'); // When true, we must disable any speech-recognition behavior and hide the voice toggle. window._MOS_DISABLE_VOICE = !!(_isAndroid || _isIOS); // Ensure voice toggle is hidden on mobile (you already set window._MOS_DISABLE_VOICE) try { const voiceToggleBtn = document.getElementById('voiceToggle'); if (voiceToggleBtn && window._MOS_DISABLE_VOICE) { voiceToggleBtn.style.display = 'none'; } } catch(e){} function sendSync(obj){ try { if (window.MOS_SYNC_API && typeof MOS_SYNC_API.send === 'function') { MOS_SYNC_API.send(obj); } } catch(e) { /* ignore if API not ready */ } } /* ---------- Show / Hide chat UI (Teacher) ---------- (Kept unchanged — same as your original, safe to leave) */ function installShowHideForTeacher() { try { var chat = document.getElementById('mos-chat'); var hideBtn = document.getElementById('mos-hide'); var showBtn = document.getElementById('mos-show'); if (!chat) return; // Ensure show button exists (already in HTML, but if missing create) if (!showBtn) { showBtn = document.createElement('button'); showBtn.id = 'mos-show'; showBtn.className = 'mos-show'; showBtn.innerText = '⊞'; showBtn.title = 'Show chat'; showBtn.setAttribute('aria-hidden', 'true'); document.body.appendChild(showBtn); } function moveShowIntoContainerIfFullscreen() { // NOTE: since we force pseudo-fullscreen everywhere, this function still keeps // the show button inside container when pseudo fullscreen is active. var isFull = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); try { if (isFull && videoContainer && videoContainer.contains(document.fullscreenElement || null)) { // move into container for visibility if (!videoContainer.contains(showBtn)) { document.body.removeChild(showBtn); videoContainer.appendChild(showBtn); showBtn.style.display = showBtn._visible ? 'block' : 'none'; } } else { // move back to body if (videoContainer.contains(showBtn)) { videoContainer.removeChild(showBtn); document.body.appendChild(showBtn); showBtn.style.display = showBtn._visible ? 'block' : 'none'; } } } catch (e) { /* ignore */ } } // hide action: store previous geometry & hide function hideChat() { try { // Save inline style snapshot so restore can return to previous position/size var snap = { left: chat.style.left || null, top: chat.style.top || null, width: chat.style.width || null, height: chat.style.height || null, position: chat.style.position || null, display: chat.style.display || null }; chat.dataset._hideSnap = JSON.stringify(snap); chat.setAttribute('data-mos-hidden', '1'); // show the floating show button showBtn._visible = true; showBtn.style.display = 'block'; showBtn.setAttribute('aria-hidden', 'false'); // If showBtn is inside container and container is fullscreen, keep it visible (CSS handles placement) moveShowIntoContainerIfFullscreen(); } catch (e) {} } function showChat() { try { var snap = null; if (chat.dataset._hideSnap) { try { snap = JSON.parse(chat.dataset._hideSnap); } catch (e) { snap = null; } } chat.removeAttribute('data-mos-hidden'); if (snap) { // restore inline styles only if they existed before hide (nulls are ignored) if (snap.left !== null) chat.style.left = snap.left; else chat.style.left = chat.style.left || ''; if (snap.top !== null) chat.style.top = snap.top; else chat.style.top = chat.style.top || ''; if (snap.width !== null) chat.style.width = snap.width; else chat.style.width = chat.style.width || ''; if (snap.height !== null) chat.style.height = snap.height; else chat.style.height = chat.style.height || ''; if (snap.position !== null) chat.style.position = snap.position; else chat.style.position = chat.style.position || ''; } // hide floating show button showBtn._visible = false; showBtn.style.display = 'none'; showBtn.setAttribute('aria-hidden', 'true'); // If showBtn was moved to container, keep it but hidden — move back to body on exit fullscreen handler moveShowIntoContainerIfFullscreen(); } catch (e) {} } // Wire buttons if (hideBtn) { hideBtn.addEventListener('click', function (ev) { ev && ev.preventDefault && ev.preventDefault(); hideChat(); }); } showBtn.addEventListener('click', function (ev) { ev && ev.preventDefault && ev.preventDefault(); showChat(); }); // Keep show button position when entering/exiting fullscreen document.addEventListener('fullscreenchange', moveShowIntoContainerIfFullscreen); document.addEventListener('webkitfullscreenchange', moveShowIntoContainerIfFullscreen); document.addEventListener('mozfullscreenchange', moveShowIntoContainerIfFullscreen); document.addEventListener('MSFullscreenChange', moveShowIntoContainerIfFullscreen); // initialize visibility based on chat attribute if (chat.getAttribute('data-mos-hidden') === '1') { showBtn._visible = true; showBtn.style.display = 'block'; showBtn.setAttribute('aria-hidden', 'false'); } else { showBtn._visible = false; showBtn.style.display = 'none'; showBtn.setAttribute('aria-hidden', 'true'); } } catch (e) { // keep errors visible if (console && console.error) console.error('installShowHideForTeacher error', e); } } /* ---------- draggable + maximize + reset for .mos-chat ---------- (unchanged) */ function makeChatDraggable() { try { var chat = document.getElementById('mos-chat'); var header = document.getElementById('mos-chat-header'); var btnReset = document.getElementById('mos-reset'); var btnToggle = document.getElementById('mos-toggle-size'); if (!chat || !header) return; // default (restore) values var DEFAULT = { left: 12, top: 12, width: 320, height: 200 }; // save defaults into dataset if not present if (!chat.dataset.mosDefault) { chat.dataset.mosDefault = JSON.stringify(DEFAULT); } var dragging = false; var startX = 0, startY = 0; var startLeft = 0, startTop = 0; function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } header.addEventListener('pointerdown', function (ev) { // only left button or touch if (ev.button !== undefined && ev.button !== 0) return; // If the pointerdown happened on an interactive control inside the header, // do NOT start dragging — allow the control to receive click events. try { var tgt = ev.target; if (tgt) { var tag = (tgt.tagName || '').toLowerCase(); var isControl = tag === 'button' || tag === 'select' || tag === 'input' || tag === 'textarea' || tag === 'label' || (tag === 'a' && tgt.getAttribute('href')) || tgt.closest && tgt.closest('[data-mos-no-drag], button, select, input, textarea, label, a'); if (isControl) { // let the control handle the event (do not preventDefault or capture) return; } } } catch (e) { // ignore detection errors and continue to start drag } // Prevent default only for genuine drag starts try { ev.preventDefault(); } catch (e) {} try { header.setPointerCapture && header.setPointerCapture(ev.pointerId); } catch (e) {} dragging = true; // start coords (viewport) startX = ev.clientX; startY = ev.clientY; // current position (parse computed style) var rect = chat.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; // add dragging class for styling if needed chat.classList.add('mos-dragging'); }); window.addEventListener('pointermove', function (ev) { if (!dragging) return; ev.preventDefault(); // delta relative to pointer start var dx = ev.clientX - startX; var dy = ev.clientY - startY; // compute new top/left (viewport coordinates) var newLeft = Math.round(startLeft + dx); var newTop = Math.round(startTop + dy); // keep inside viewport boundaries var vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); var vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); var rect = chat.getBoundingClientRect(); var w = rect.width; var h = rect.height; newLeft = clamp(newLeft, 6, Math.max(6, vw - Math.max(200, w) - 6)); newTop = clamp(newTop, 6, Math.max(6, vh - Math.max(120, h) - 6)); // apply chat.style.left = newLeft + 'px'; chat.style.top = newTop + 'px'; chat.style.right = 'auto'; chat.style.bottom = 'auto'; }); function endDrag(ev) { if (!dragging) return; dragging = false; try { header.releasePointerCapture && header.releasePointerCapture(ev && ev.pointerId); } catch(e) {} chat.classList.remove('mos-dragging'); } header.addEventListener('pointerup', endDrag); header.addEventListener('pointercancel', endDrag); // Make sure pointerup on window also stops dragging (safety) window.addEventListener('pointerup', endDrag); /* --- Reset button: restore default pos/size --- */ if (btnReset) { btnReset.addEventListener('click', function (e) { e.preventDefault(); var def = DEFAULT; try { var ds = chat.dataset.mosDefault; if (ds) def = JSON.parse(ds); } catch (ex) {} chat.style.left = (def.left || 12) + 'px'; chat.style.top = (def.top || 12) + 'px'; chat.style.width = (def.width || 320) + 'px'; chat.style.height = (def.height || 200) + 'px'; chat.style.right = 'auto'; chat.style.bottom = 'auto'; }); } /* --- Robust Maximize / Restore toggle --- */ if (btnToggle) { btnToggle.addEventListener('click', function (e) { try { e.preventDefault(); } catch(e){} var isMax = chat.dataset.mosMax === '1'; if (!isMax) { // Save current metrics (viewport values) try { var rect = chat.getBoundingClientRect(); chat.dataset._prevLeft = Math.round(rect.left); chat.dataset._prevTop = Math.round(rect.top); chat.dataset._prevWidth = Math.round(rect.width); chat.dataset._prevHeight = Math.round(rect.height); // store current position style (so we can restore) var cs = window.getComputedStyle(chat); chat.dataset._prevPosition = cs.position || 'fixed'; } catch (ex) { /* ignore */ } // Maximize to viewport with small margin var vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); var vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); // ensure fixed positioning chat.style.position = 'fixed'; chat.style.left = '6px'; chat.style.top = '6px'; chat.style.width = (vw - 12) + 'px'; chat.style.height = (vh - 12) + 'px'; chat.style.right = 'auto'; chat.style.bottom = 'auto'; chat.dataset.mosMax = '1'; } else { // Restore previous rect if present, otherwise fallback to default try { var prevLeft = chat.dataset._prevLeft; var prevTop = chat.dataset._prevTop; var prevWidth = chat.dataset._prevWidth; var prevHeight = chat.dataset._prevHeight; var prevPos = chat.dataset._prevPosition || 'fixed'; if (prevLeft !== undefined && prevTop !== undefined && prevWidth !== undefined && prevHeight !== undefined) { chat.style.position = prevPos; chat.style.left = Math.round(Number(prevLeft)) + 'px'; chat.style.top = Math.round(Number(prevTop)) + 'px'; chat.style.width = Math.round(Number(prevWidth)) + 'px'; chat.style.height = Math.round(Number(prevHeight)) + 'px'; } else { var ds = chat.dataset.mosDefault ? JSON.parse(chat.dataset.mosDefault) : DEFAULT; chat.style.position = 'fixed'; chat.style.left = (ds.left || DEFAULT.left) + 'px'; chat.style.top = (ds.top || DEFAULT.top) + 'px'; chat.style.width = (ds.width || DEFAULT.width) + 'px'; chat.style.height = (ds.height || DEFAULT.height) + 'px'; } } catch (ex) { // fallback: reset to defaults var ds2 = chat.dataset.mosDefault ? JSON.parse(chat.dataset.mosDefault) : DEFAULT; chat.style.position = 'fixed'; chat.style.left = (ds2.left || DEFAULT.left) + 'px'; chat.style.top = (ds2.top || DEFAULT.top) + 'px'; chat.style.width = (ds2.width || DEFAULT.width) + 'px'; chat.style.height = (ds2.height || DEFAULT.height) + 'px'; } chat.dataset.mosMax = '0'; } }); } /* Accessibility: allow keyboard reset (R) when focused on chat */ chat.tabIndex = chat.tabIndex || -1; chat.addEventListener('keydown', function (ev) { if (ev.key === 'r' || ev.key === 'R') { var evt = new MouseEvent('click'); btnReset && btnReset.dispatchEvent(evt); } }); // ensure chat stays on top if clicked chat.addEventListener('mousedown', function () { chat.style.zIndex = 999999; }); } catch (e) { if (console && console.error) console.error('makeChatDraggable error', e); } } /* ---------- Chat init for TEACHER (uses #mos-jitsi-placeholder only) ---------- (unchanged) */ function initChatTeacher() { try { var status = document.getElementById('mos-chat-status'); if (status) status.textContent = 'Loading video chat'; // room name based on session var sid = (window._SYNC_RUNTIME && window._SYNC_RUNTIME.sessionId) || window.SYNC_USERNAME || 'default_session'; var teacherId = (window._SYNC_RUNTIME && window._SYNC_RUNTIME.teacherId) || window.SYNC_TEACHER || 'teacher'; sid = String(sid).replace(/[^A-Za-z0-9_-]/g,'_'); teacherId = String(teacherId).replace(/[^A-Za-z0-9_-]/g,'_'); var roomName = 'mos-' + sid + '-room'; // placeholder inside chat (only place for Jitsi) var placeholder = document.getElementById('mos-jitsi-placeholder'); if (!placeholder) { if (status) status.textContent = 'No chat container'; return; } // Ensure the Jitsi external API is present (WP Coder should load it as external script) function ensureJitsiApiAndCreate(attemptsLeft) { attemptsLeft = typeof attemptsLeft === 'number' ? attemptsLeft : 8; if (typeof window.JitsiMeetExternalAPI === 'undefined') { if (attemptsLeft <= 0) { if (status) status.textContent = 'Jitsi API not available'; if (console && console.error) console.error('mos-chat: Jitsi External API not found on window. Please add external_api.js via WP Coder external scripts (meet.ffmuc.net or meet.jit.si).'); return; } setTimeout(function(){ ensureJitsiApiAndCreate(attemptsLeft - 1); }, 600); return; } // create jitsi now that API is present createJitsiOnceParentReady(8); } // create Jitsi when placeholder is in the DOM function createJitsiOnceParentReady(attemptsLeft) { attemptsLeft = typeof attemptsLeft === 'number' ? attemptsLeft : 6; var parent = placeholder; if (!parent || !document.contains(parent)) { if (attemptsLeft <= 0) { if (status) status.textContent = 'Chat init failed'; return; } setTimeout(function(){ createJitsiOnceParentReady(attemptsLeft-1); }, 300); return; } try { window._MOS_JITSI = window._MOS_JITSI || {}; // Dispose previous if exists try { if (window._MOS_JITSI.api) { window._MOS_JITSI.api.dispose(); } } catch(e){} // Prefer server domain for instantiation — use meet.ffmuc.net by default var server = 'meet.ffmuc.net'; var options = { roomName: roomName, parentNode: parent, width: '100%', height: '100%', configOverwrite: { startWithAudioMuted: false, startWithVideoMuted: false, disableDeepLinking: true }, interfaceConfigOverwrite: { TOOLBAR_BUTTONS: [ 'microphone','camera','tileview','videobackgroundblur','fullscreen','hangup' ] }, userInfo: { displayName: teacherId } }; window._MOS_JITSI.api = new JitsiMeetExternalAPI(server, options); var api = window._MOS_JITSI.api; if (status) status.textContent = 'Room: ' + roomName + ' (teacher)'; // participants dropdown var select = document.getElementById('mos-participants'); function refreshParticipants() { try { if (!api || !select) return; var parts = api.getParticipantsInfo() || []; select.innerHTML = ''; var optAll = document.createElement('option'); optAll.value = '__all'; optAll.textContent = 'All / Teacher'; select.appendChild(optAll); for (var i=0;i response.text()) .then(text => { const file = text; const { times, numbers } = processVideoTimes(file); const isMobilePseudoFS = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); var playbackspeed = vid.playbackRate; let isPseudoFullscreen = false; var timeIndex = []; let playloop = ["false"]; let playwithoutstop = ["false"]; let timeplay = 0; let controljumps = ["true"]; let controljumsonseek = ["false"]; let onclick = ["false"]; let onclickm = ["false"]; let p = 10; let timeoutid; let intervalid; let intervalidonstop; let intervalidonstartstop; let intervalidonstopstart; let intervalidonclick; let intervalrestarting = setInterval(restarting, 15000); let forloop = 0; let afterthought = ["false"]; let n = 1; let h = ["false"]; let textnumbers = null; let measures = null; let number = null; let seconds = null; let vidplay = ["true"]; let voiceEnabled = true; const voiceToggleBtn = document.getElementById('voiceToggle'); // If voice is disabled for this OS, force it OFF and hide the toggle button. if (window._MOS_DISABLE_VOICE) { voiceEnabled = false; try { if (voiceToggleBtn) voiceToggleBtn.style.display = 'none'; } catch(e){} } for (let t = 1; t <= numbers; t++) { timeIndex[t] = ["false"]; } timeIndex[0] = ["true"]; vid.addEventListener("timeupdate", function (pauseVideo) { if (afterthought == "true") { forloop = timeplay; } else { forloop = 0; } for (let i = forloop; i <= numbers; i += n) { if (vid.currentTime >= (times[i] * playbackspeed) && vid.currentTime < (times[i + 1] * playbackspeed) && timeIndex[i] == "false" && playloop == "false") { if (controljumsonseek == "false" && playwithoutstop == "false") { vid.pause(); timeIndex[i] = ["true"]; timeplay = i; controljumps = ["false"]; } if (controljumsonseek == "true" || playwithoutstop == "true") { timeIndex[i] = ["true"]; timeplay = i; controljumps = ["false"]; controljumsonseek = ["false"]; } } } if (playloop == "true") { if (vid.currentTime >= (times[measures[1] - 1] * playbackspeed) || vid.currentTime < ((times[measures[0] - 1] * playbackspeed) - 0.001)) { controljumps = ["true"]; vid.pause(); vid.currentTime = (times[measures[0] - 1] * playbackspeed); } } }); vid.addEventListener("ended", (event) => { playwithoutstop = ["false"]; playloop = ["false"]; vid.currentTime = 0; vid.pause(); for (let d = 1; d <= numbers; d++) { timeIndex[d] = ["false"]; } timeIndex[0] = ["true"]; timeplay = 0; controljumsonseek = ["false"]; clearAll(); timeoutid = setTimeout(repeatplay, p * 1000); // Broadcast ended/pause+seek to students sendSync({ action: "pause", time: 0 }); sendSync({ action: "seek", time: 0 }); sendSync({ action: "ended", time: 0 }); }); vid.addEventListener('contextmenu', event => { event.preventDefault(); }); // sanity check if (!videoContainer || !vid || !button) { console.error('Video container, video, or button not found'); return; } // --- Fullscreen / Pseudo-Fullscreen Logic --- // Use existing global consts: videoContainer, vid, button // Improve iOS behavior: force playsinline, prevent auto-native-fullscreen, use pseudo fullscreen const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); const isAndroid = /Android/i.test(navigator.userAgent); // Ensure inline playback attributes exist BEFORE any play() calls try { vid.setAttribute('playsinline', ''); vid.setAttribute('webkit-playsinline', ''); vid.setAttribute('controlsList', 'nodownload nofullscreen'); // remove native controls initially to avoid some iOS auto-fullscreen behaviors vid.removeAttribute('controls'); vid.controls = false; } catch (e) { // ignore attribute failures } function enterPseudo() { if (!videoContainer.classList.contains('mos-pseudo-fullscreen')) { videoContainer.classList.add('mos-pseudo-fullscreen'); } // lock body scroll document.body.classList.add('no-scroll'); isPseudoFullscreen = true; // ensure .mos-show inside container is visible const showBtns = videoContainer.querySelectorAll('.mos-show'); showBtns.forEach(b => { b.style.display = 'block'; }); // enable video controls for pseudo (safe because playsinline is set) try { vid.controls = true; } catch (e) {} } function exitPseudo() { videoContainer.classList.remove('mos-pseudo-fullscreen'); document.body.classList.remove('no-scroll'); isPseudoFullscreen = false; try { vid.controls = false; } catch (e) {} } function enterNative() { // Prefer native fullscreen on desktop/Android if (videoContainer.requestFullscreen) { videoContainer.requestFullscreen(); } else if (videoContainer.webkitRequestFullscreen) { videoContainer.webkitRequestFullscreen(); } else if (videoContainer.mozRequestFullScreen) { videoContainer.mozRequestFullScreen(); } else if (videoContainer.msRequestFullscreen) { videoContainer.msRequestFullscreen(); } } function exitNative() { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } // Toggle unified: iOS -> pseudo, others prefer native (fallback to pseudo) function toggleFullscreenUnified() { // If iOS -> always pseudo if (isIOS) { if (!isPseudoFullscreen) enterPseudo(); else exitPseudo(); return; } // If Android or other desktop: try native if available const nativeSupported = !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.msFullscreenEnabled); const inNative = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); if (nativeSupported) { if (!inNative) { enterNative(); } else { exitNative(); } } else { // fallback to pseudo if (!isPseudoFullscreen) enterPseudo(); else exitPseudo(); } } // Attach single click handler to the big button (replacing older duplicate handlers) // Prevent default where other code expects it button.addEventListener('click', (ev) => { try { if (ev && ev.preventDefault) ev.preventDefault(); } catch(e){} toggleFullscreenUnified(); }); // ESC key handling to exit pseudo fullscreen document.addEventListener('keydown', (e) => { if ((e.key === 'Escape' || e.key === 'Esc') && isPseudoFullscreen) { exitPseudo(); } }); // Keep UI consistent on native fullscreen change function onNativeFsChange() { const inNative = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement); if (inNative) { // Hide the big button in native fullscreen (consistent with existing behavior) try { button.style.display = 'none'; } catch (e) {} try { vid.classList.remove('show-controls'); vid.classList.add('hide-controls'); } catch (e) {} } else { try { button.style.display = 'block'; } catch (e) {} } } document.addEventListener('fullscreenchange', onNativeFsChange); document.addEventListener('webkitfullscreenchange', onNativeFsChange); document.addEventListener('mozfullscreenchange', onNativeFsChange); document.addEventListener('MSFullscreenChange', onNativeFsChange); // Also handle the other existing fullscreenchange->handleFullscreenChange logic already in file. // Remove duplicate tryEnter/tryExit calls (they were undefined); we already installed a unified handler above. // ensure if iOS somehow triggers webkitbeginfullscreen we exit native and go pseudo vid.addEventListener('webkitbeginfullscreen', function () { try { if (vid.webkitExitFullscreen) vid.webkitExitFullscreen(); } catch (e) {} setTimeout(() => { if (!videoContainer.classList.contains('mos-pseudo-fullscreen')) enterPseudo(); }, 50); }); // MutationObserver: if something hides .mos-show, force it visible while in pseudo fullscreen // Ensure .mos-show remains visible and clickable in pseudo fullscreen const mo = new MutationObserver(function () { if (videoContainer.classList.contains('mos-pseudo-fullscreen')) { const showBtns = videoContainer.querySelectorAll('.mos-show'); showBtns.forEach(b => { // force visible with !important b.style.setProperty('display', 'block', 'important'); b.style.setProperty('pointer-events', 'auto', 'important'); b.style.setProperty('z-index', '2147483647', 'important'); }); } }); try { mo.observe(videoContainer, { attributes: true, subtree: true, attributeFilter: ['style', 'class', 'data-mos-hidden'] }); } catch (e) { console.warn('MutationObserver attach failed', e); } // === End fullscreen/pseudo-fullscreen section === // The remainder of your original code continues unchanged (event listeners, controls, annyang, etc.) // (existing listeners and functions from your original snippet follow...) vid.addEventListener('seeking', function (event) { vid.classList.remove('show-controls'); }); vid.addEventListener('seeked', function (event) { clearAll(); stopprepare(); afterthought = ["true"]; if (onclick == "true") { controljumps = ["truefalse"]; } if (onclick == "truefalse") { controljumps = ["true"]; } if (onclick == "false") { controljumps = ["false"]; } vid.pause(); clearTimeout(intervalidonclick); intervalidonclick = setTimeout(showhide, 8000); vid.focus(); // Broadcast seek to students (teacher) try { sendSync({ action: "seek", time: vid.currentTime }); } catch (e) {} }); document.addEventListener('fullscreenchange', handleFullscreenChange); document.addEventListener('webkitfullscreenchange', handleFullscreenChange); document.addEventListener('mozfullscreenchange', handleFullscreenChange); document.addEventListener('MSFullscreenChange', handleFullscreenChange); function handleFullscreenChange() { if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) { button.style.display = 'none'; vid.classList.remove('show-controls'); setTimeout(() => { vid.classList.remove('hide-controls'); vid.classList.add('show-controls'); }, 4000); } else { button.style.display = 'block'; } vid.focus(); } vid.addEventListener('volumechange', (event) => { vid.focus(); }); function repeatplay() { vid.play(); vidplay = ["false"]; // Broadcast play to students (teacher) try { sendSync({ action: "play", time: vid.currentTime }); } catch (e) {} } function restarting() { if (!voiceEnabled) return; if (annyang) { if (annyang.isListening()) { annyang.abort(); } annyang.start({ autoRestart: true, continuous: true, paused: false }); } } function showhide() { vid.classList.remove('hide-controls'); vid.classList.add('show-controls'); } function aborting() { if (!voiceEnabled) return; if (annyang) { if (annyang.isListening()) { annyang.abort(); } } } function starting() { if (!voiceEnabled) return; if (annyang) { annyang.start({ autoRestart: true, continuous: true, paused: false }); } } function clearAll() { clearTimeout(timeoutid); clearInterval(intervalrestarting); clearInterval(intervalid); clearInterval(intervalidonstop); clearInterval(intervalidonstartstop); clearInterval(intervalidonstopstart); } function controljumpstrue() { onclick = ["truefalse"]; controljumps = ["true"]; vidplay = ["false"]; if (timeplay < numbers && timeplay >= 0) { controljumsonseek = ["truefalse"]; vid.currentTime = (times[timeplay] * playbackspeed); } else { controljumsonseek = ["true"]; } // broadcast seek when jumping by measure change try { sendSync({ action: "seek", time: vid.currentTime }); } catch (e) {} } function controljumpsfalse() { onclick = ["truefalse"]; playwithoutstop = ["false"]; playloop = ["false"]; controljumps = ["true"]; controljumsonseek = ["truefalse"]; vidplay = ["false"]; for (o = 0; o <= numbers; o++) { timeIndex[o] = ["false"]; } afterthought = ["true"]; vid.currentTime = (times[timeplay] * playbackspeed); starting(); } function controljumpsfalsetrue() { controljumsonseek = ["false"]; if (measures[0] < numbers && measures[0] >= 0) { onclick = ["truefalse"]; controljumps = ["true"]; vid.currentTime = (times[measures[0] - 1] * playbackspeed); playloop = ["true"]; } else { playloop = ["false"]; } starting(); } function controljumpstruefalse() { if (measures[1] < numbers && measures[1] >= 0) { timeplay = measures[1] - 1; } else { timeplay = measures[0] - 1; } onclick = ["truefalse"]; playloop = ["false"]; controljumps = ["true"]; vidplay = ["false"]; controljumsonseek = ["truefalse"]; vid.currentTime = (times[timeplay] * playbackspeed); } function videoplay() { if (vidplay == "true") { vid.play(); vidplay = ["false"]; // Broadcast play to students (teacher) try { sendSync({ action: "play", time: vid.currentTime }); } catch (e) {} } else { clearAll(); timeoutid = setTimeout(repeatplay, 2000); } } function stopping() { vid.pause(); onclick = ["true"]; playwithoutstop = ["false"]; controljumps = ["truefalse"]; vidplay = ["false"]; controljumsonseek = ["truefalse"]; vid.currentTime = (times[timeplay] * playbackspeed); // Broadcast pause to students (teacher) try { sendSync({ action: "pause", time: vid.currentTime }); } catch (e) {} } function stopprepare() { playwithoutstop = ["false"]; vidplay = ["false"]; controljumsonseek = ["truefalse"]; } if (annyang) { var commands = { 'start': playing, 'kreni': playing, 'jedan takt unazad': minus_one_bar, 'jedan takt unapred': plus_one_bar, 'dva takta unazad': minus_two_bars, 'dva takta unapred': plus_two_bars, 'ponavljaj taktove': ponavljanje, 'zaustavi ponavljanje': zaustavi_petlju, 'počni od početka': od_pocetaka, 'kreni bez zaustavljanja': bez_zaustavljanja, 'dužina zaustavljanja': duzina_zaustavljanja, 'priprema': priprema, 'minus': minus, 'plus': plus, 'stop': stop, 'brzina': setspeed }; } else { // Suppress the alert on Android/iOS where voice is intentionally disabled. if (!window._MOS_DISABLE_VOICE) { window.alert("Prepoznavanje glasa ne radi u ovom pretraživaču!!!"); } } function playing() { if (playwithoutstop == "false") { vid.play(); vidplay = ["false"]; // Broadcast play to students (teacher) try { sendSync({ action: "play", time: vid.currentTime }); } catch (e) {} } } function minus_one_bar() { if (playloop == "false" && playwithoutstop == "false") { timeplay = timeplay - 1; controljumpstrue(); } } function minus() { if (playloop == "false" && playwithoutstop == "false") { timeplay = timeplay - n; controljumpstrue(); } } function plus() { if (playloop == "false" && playwithoutstop == "false") { timeplay = timeplay + n; controljumpstrue(); } } function plus_one_bar() { if (playloop == "false" && playwithoutstop == "false") { timeplay = timeplay + 1; controljumpstrue(); } } function minus_two_bars() { if (playloop == "false" && playwithoutstop == "false") { timeplay = timeplay - 2; controljumpstrue(); } } function plus_two_bars() { if (playloop == "false" && playwithoutstop == "false") { timeplay = timeplay + 2; controljumpstrue(); } } function ponavljanje() { if (playwithoutstop == "false") { vid.pause(); aborting(); clearAll(); try { sendSync({ action: "pause", time: vid.currentTime }); } catch (e) {} while (textnumbers == null || textnumbers == "") { textnumbers = prompt("Unesi taktove koje hoćeš da se ponavljaju, na primer: 10 - 20:"); } measures = textnumbers.split('-'); var textnumbers = null; onclickm = ["true"]; controljumpsfalsetrue(); } } function zaustavi_petlju() { if (playloop == "true" && playwithoutstop == "false") { vid.pause(); controljumpstruefalse(); } } function od_pocetaka() { playwithoutstop = ["false"]; playloop = ["false"]; vid.pause(); vid.currentTime = 0; for (t = 0; t <= numbers; t++) { timeIndex[t] = ["false"]; } if (controljumsonseek == "false") { timeIndex[0] = ["true"]; } controljumps = ["true"]; vidplay = ["false"]; timeplay = 0; clearAll(); intervalrestarting = setInterval(restarting, 15000); } function bez_zaustavljanja() { playwithoutstop = ["true"]; playloop = ["false"]; n = 1; afterthought = ["false"]; videoplay(); } function duzina_zaustavljanja() { if (playloop == "false" && playwithoutstop == "false") { aborting(); stopping(); clearAll(); while (number == null || number == "") { number = prompt("Unesi broj taktova između zaustavljanja:"); } n = parseInt(number, 10); var number = null; starting(); } } function priprema() { if (playloop == "false" && playwithoutstop == "false") { aborting(); stopping(); clearAll(); while (seconds == null || seconds == "" || seconds < 3) { seconds = prompt("Unesi koliko sekundi traje automatska priprema. Unet broj treba da bude veći ili jednak broju 3:"); } p = parseInt(seconds, 10); var seconds = null; starting(); } } function stop() { stopping(); } function setspeed() { aborting(); stopping(); clearAll(); let input = prompt("Unesite brzinu reprodukcije (npr. 1, 1.25, 0.5). Minimum 0.1:"); if (input === null) return; input = input.trim(); if (input === "") return; const val = parseFloat(input); if (isNaN(val) || val <= 0) { alert("Nevažeća vrednost brzine."); return; } const safeVal = Math.max(0.1, val); vid.playbackRate = safeVal; var playbackspeed = vid.playbackRate; alert("Brzina reprodukcije je podešena na " + vid.playbackRate); starting(); // Broadcast speed change to students (teacher) try { sendSync({ action: "speed", rate: vid.playbackRate }); } catch (e) {} } if (annyang) { annyang.addCommands(commands); annyang.setLanguage('sr-SP'); if (voiceEnabled) { annyang.start({ autoRestart: true, continuous: true, paused: false }); } } if (voiceToggleBtn) { voiceToggleBtn.addEventListener('click', () => { // No-op on Android/iOS where voice is intentionally disabled if (window._MOS_DISABLE_VOICE) return; voiceEnabled = !voiceEnabled; voiceToggleBtn.textContent = voiceEnabled ? 'Voice: ON' : 'Voice: OFF'; if (voiceEnabled) { if (annyang) { try { annyang.start({ autoRestart: true, continuous: true, paused: false }); } catch (e) {} } } else { if (annyang && annyang.isListening()) { annyang.abort(); } } }); } const button1 = document.getElementById('Play'); const button2 = document.getElementById('Preparation'); const button3 = document.getElementById('BarsBetweenStops'); const button4 = document.getElementById('Minus'); const button5 = document.getElementById('Plus'); const button6 = document.getElementById('OneBarBack'); const button7 = document.getElementById('OneBarAhead'); const button8 = document.getElementById('TwoBarsBack'); const button9 = document.getElementById('TwoBarsForward'); const button10 = document.getElementById('RepeatBars'); const button11 = document.getElementById('StopRepeating'); const button12 = document.getElementById('PlayWithoutStopping'); const button13 = document.getElementById('StartFromBeginning'); const button14 = document.getElementById('Stop'); const button15 = document.getElementById('SetSpeed'); if (button1) button1.addEventListener('click', playing); if (button2) button2.addEventListener('click', priprema); if (button3) button3.addEventListener('click', duzina_zaustavljanja); if (button4) button4.addEventListener('click', minus); if (button5) button5.addEventListener('click', plus); if (button6) button6.addEventListener('click', minus_one_bar); if (button7) button7.addEventListener('click', plus_one_bar); if (button8) button8.addEventListener('click', minus_two_bars); if (button9) button9.addEventListener('click', plus_two_bars); if (button10) button10.addEventListener('click', ponavljanje); if (button11) button11.addEventListener('click', zaustavi_petlju); if (button12) button12.addEventListener('click', bez_zaustavljanja); if (button13) button13.addEventListener('click', od_pocetaka); if (button14) button14.addEventListener('click', stop); if (button15) button15.addEventListener('click', setspeed); document.addEventListener('keydown', (event) => { if (event.key == 'k' || event.key == 'K') { if (playwithoutstop == "false") { videoplay(); } } if (event.key == 's' || event.key == 'S') { stopping(); } if (event.key == 't' || event.key == 'T') { minus_one_bar(); } if (event.key == 'i' || event.key == 'I') { plus_one_bar(); } if (event.key == 'e' || event.key == 'E') { minus_two_bars(); } if (event.key == 'p' || event.key == 'P') { plus_two_bars(); } if (event.key == 'm' || event.key == 'M') { ponavljanje(); } if (event.key == 'b' || event.key == 'B') { zaustavi_petlju(); } if (event.key == 'q' || event.key == 'Q') { od_pocetaka(); } if (event.key == 'x' || event.key == 'X') { bez_zaustavljanja(); } if (event.key == 'd' || event.key == 'D') { duzina_zaustavljanja(); } if (event.key == '-') { minus(); } if (event.key == '+') { plus(); } if (event.key == 'h' || event.key == 'H') { priprema(); } if (event.key == 'c' || event.key == 'C') { setspeed(); } }); vid.onpause = function (recognitionstart) { clearAll(); if (playwithoutstop == "true") { playwithoutstop = ["false"]; } starting(); timeoutid = setTimeout(repeatplay, p * 1000); if (vidplay == "false") { let remainingTime = 2000; intervalid = setInterval(() => { remainingTime -= 100; if (remainingTime <= 0) { clearInterval(intervalid); vidplay = ["true"]; } }, 100); } try { sendSync({ action: "pause", time: vid.currentTime }); } catch (e) {} }; vid.onplaying = function (recognitionstop) { clearAll(); aborting(); vidplay = ["false"]; // Also broadcast play (safety net) try { sendSync({ action: "play", time: vid.currentTime }); } catch (e) {} }; vid.onseeked = function (seeking) { clearAll(); if (controljumps == "false") { afterthought = ["false"]; vidplay = ["false"]; let remainingTimeonstopstart = 2000; intervalidonstopstart = setInterval(() => { remainingTimeonstopstart -= 100; if (remainingTimeonstopstart <= 0) { clearInterval(intervalidonstopstart); vidplay = ["true"]; } }, 100); for (k = 0; k <= numbers; k++) { timeIndex[k] = ["false"]; } for (let x = 0; x <= numbers; x++) { if (vid.currentTime >= (times[x] * playbackspeed) && vid.currentTime < (times[x + 1] * playbackspeed)) { timeplay = x; } } intervalrestarting = setInterval(restarting, 15000); } if (controljumps == "true") { onclick = ["false"]; if (vidplay == "false") { let remainingTimeonstartstop = 2000; intervalidonstartstop = setInterval(() => { remainingTimeonstartstop -= 100; if (remainingTimeonstartstop <= 0) { clearInterval(intervalidonstartstop); vidplay = ["true"]; } }, 100); } else { vidplay = ["true"]; } afterthought = ["true"]; for (k = 0; k <= numbers; k++) { timeIndex[k] = ["false"]; } if (onclickm == "true") { intervalrestarting = setInterval(restarting, 15000); onclickm = ["false"]; } else { timeoutid = setTimeout(repeatplay, p * 1000); } } if (controljumps == "truefalse") { onclick = ["false"]; if (vidplay == "false") { let remainingTimeonstop = 2000; intervalidonstop = setInterval(() => { remainingTimeonstop -= 100; if (remainingTimeonstop <= 0) { clearInterval(intervalidonstop); vidplay = ["true"]; } }, 100); } else { vidplay = ["true"]; } afterthought = ["true"]; for (k = 0; k <= numbers; k++) { timeIndex[k] = ["false"]; } intervalrestarting = setInterval(restarting, 15000); } controljumsonseek = ["true"]; controljumps = ["false"]; vid.focus(); }; }); }