(function () { var form = document.getElementById('onboarding-pair-form'); var pairingCard = document.getElementById('onboarding-pair-card'); var pairingProgress = document.getElementById('onboarding-pair-progress'); var message = document.getElementById('onboarding-pair-message'); var codeInputsContainer = document.getElementById('onboarding-pair-code-inputs'); var codeField = document.getElementById('onboarding-pair-code'); var clientIdField = document.getElementById('onboarding-pair-client-id'); var anotherButton = document.getElementById('onboarding-pair-another'); var anotherButtonLabel = document.getElementById('onboarding-pair-another-label'); var manualButton = document.getElementById('onboarding-pair-manual'); var connectButton = document.getElementById('onboarding-pair-connect'); var clientListButton = document.getElementById('onboarding-pair-client-list'); var scanner = document.getElementById('onboarding-scanner'); var scannerVideo = document.getElementById('onboarding-scanner-video'); var scannerCapture = document.getElementById('onboarding-scanner-capture'); var scannerClose = document.getElementById('onboarding-scanner-close'); var scannerMessage = document.getElementById('onboarding-scanner-message'); var scannerDebugOutput = document.getElementById('onboarding-scanner-debug'); var scannerDebug = new URLSearchParams(window.location.search).get('scanner-debug') === '1'; if (!form || !message) { return; } function getClientId() { var storageKey = 'pulse-signage-player-client-id'; var clientId = clientIdField ? String(clientIdField.value || '').trim() : ''; if (!clientId) { try { clientId = String(window.sessionStorage.getItem(storageKey) || '').trim(); } catch (_error) {} } if (!clientId) { clientId = window.crypto && typeof window.crypto.randomUUID === 'function' ? window.crypto.randomUUID() : 'client-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 12); try { window.sessionStorage.setItem(storageKey, clientId); } catch (_error) {} } if (clientIdField) { clientIdField.value = clientId; } return clientId; } getClientId(); function traceScanner(value) { if (!scannerDebug || !scannerDebugOutput) { return; } scannerDebugOutput.classList.remove('d-none'); var lines = (scannerDebugOutput.textContent ? scannerDebugOutput.textContent.split('\n') : []).filter(Boolean); lines.push(new Date().toISOString().slice(11, 19) + ' ' + value); scannerDebugOutput.textContent = lines.slice(-8).join('\n'); } function wait(milliseconds) { return new Promise(function (resolve) { window.setTimeout(resolve, milliseconds); }); } function submitPairing(body, attempt) { return fetch(form.action, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: body }).then(function (response) { return response.text().then(function (text) { var payload = null; try { payload = JSON.parse(text); } catch (_error) {} var transientCodeError = response.status === 401 && payload && String(payload.error || '').indexOf('valid kiosk pairing code') !== -1; var transientTransportError = [502, 503, 504].indexOf(response.status) !== -1; if ((!response.ok && (transientCodeError || transientTransportError)) && attempt < 2) { return wait(300 * (attempt + 1)).then(function () { return submitPairing(body, attempt + 1); }); } return { response: response, payload: payload }; }); }); } var codeInputs = codeInputsContainer ? Array.prototype.slice.call(codeInputsContainer.querySelectorAll('[data-pairing-code-input]')) : []; function normalizeCode(value) { return String(value || '').replace(/[^a-z0-9]/gi, '').toUpperCase().slice(0, codeInputs.length); } function syncCodeField() { if (codeField) { codeField.value = codeInputs.map(function (input) { return input.value; }).join('').toUpperCase(); } } function setCode(value, startIndex) { var normalized = normalizeCode(value); var index = startIndex || 0; normalized.split('').forEach(function (character) { if (index < codeInputs.length) { codeInputs[index].value = character; index += 1; } }); syncCodeField(); if (index < codeInputs.length) { codeInputs[index].focus(); } else if (codeInputs.length) { codeInputs[codeInputs.length - 1].focus(); } } var scannerStream = null; var scannerFrame = null; var scannerLastDecodeAt = 0; var scannerCanvas = document.createElement('canvas'); var scannerWorkCanvas = document.createElement('canvas'); var scannerDecodeCanvas = document.createElement('canvas'); function decodeQrCanvas(canvas) { if (typeof window.jsQR !== 'function') { return null; } var workCanvas = canvas; var maximumDimension = 1024; if (Math.max(canvas.width, canvas.height) > maximumDimension) { var scale = maximumDimension / Math.max(canvas.width, canvas.height); scannerWorkCanvas.width = Math.round(canvas.width * scale); scannerWorkCanvas.height = Math.round(canvas.height * scale); scannerWorkCanvas.getContext('2d').drawImage(canvas, 0, 0, scannerWorkCanvas.width, scannerWorkCanvas.height); workCanvas = scannerWorkCanvas; } var context = workCanvas.getContext('2d', { willReadFrequently: true }); var imageData = context.getImageData(0, 0, workCanvas.width, workCanvas.height); var result = window.jsQR(imageData.data, workCanvas.width, workCanvas.height, { inversionAttempts: 'attemptBoth' }); if (result) { return result; } scannerDecodeCanvas.width = workCanvas.width; scannerDecodeCanvas.height = workCanvas.height; var decodeContext = scannerDecodeCanvas.getContext('2d', { willReadFrequently: true }); var source = new Uint8ClampedArray(imageData.data); var cleaned = new Uint8ClampedArray(source.length); for (var cleanY = 0; cleanY < workCanvas.height; cleanY += 1) { for (var cleanX = 0; cleanX < workCanvas.width; cleanX += 1) { var cleanSamples = []; for (var cleanOffsetY = -1; cleanOffsetY <= 1; cleanOffsetY += 1) { for (var cleanOffsetX = -1; cleanOffsetX <= 1; cleanOffsetX += 1) { var cleanSampleX = Math.max(0, Math.min(workCanvas.width - 1, cleanX + cleanOffsetX)); var cleanSampleY = Math.max(0, Math.min(workCanvas.height - 1, cleanY + cleanOffsetY)); cleanSamples.push(source[(cleanSampleY * workCanvas.width + cleanSampleX) * 4]); } } cleanSamples.sort(function (left, right) { return left - right; }); var cleanIndex = (cleanY * workCanvas.width + cleanX) * 4; cleaned[cleanIndex] = cleaned[cleanIndex + 1] = cleaned[cleanIndex + 2] = cleanSamples[4]; cleaned[cleanIndex + 3] = 255; } } source = cleaned; var expanded = decodeContext.createImageData(workCanvas.width, workCanvas.height); var radius = Math.max(3, Math.round(workCanvas.width / 205)); for (var y = 0; y < workCanvas.height; y += 1) { for (var x = 0; x < workCanvas.width; x += 1) { var samples = []; for (var offsetY = -radius; offsetY <= radius; offsetY += 2) { for (var offsetX = -radius; offsetX <= radius; offsetX += 2) { var sampleX = Math.max(0, Math.min(workCanvas.width - 1, x + offsetX)); var sampleY = Math.max(0, Math.min(workCanvas.height - 1, y + offsetY)); samples.push(source[(sampleY * workCanvas.width + sampleX) * 4]); } } var maximum = Math.max.apply(null, samples); var index = (y * workCanvas.width + x) * 4; expanded.data[index] = maximum; expanded.data[index + 1] = maximum; expanded.data[index + 2] = maximum; expanded.data[index + 3] = 255; } } decodeContext.putImageData(expanded, 0, 0); return window.jsQR(expanded.data, workCanvas.width, workCanvas.height, { inversionAttempts: 'attemptBoth' }); } function closeScanner() { if (scannerFrame) { window.cancelAnimationFrame(scannerFrame); scannerFrame = null; } if (scannerStream) { scannerStream.getTracks().forEach(function (track) { track.stop(); }); scannerStream = null; } if (scannerVideo) { scannerVideo.srcObject = null; scannerVideo.classList.remove('d-none'); } if (scannerCapture) { scannerCapture.value = ''; } if (scanner) { scanner.classList.add('d-none'); scanner.setAttribute('aria-hidden', 'true'); } } function resetPairingForm() { form.reset(); codeInputsContainer.setAttribute('data-pairing-code', ''); codeInputs.forEach(function (input) { input.value = ''; input.disabled = false; }); if (codeField) { codeField.value = ''; } Array.prototype.slice.call(form.elements).forEach(function (element) { if (element !== anotherButton && element !== manualButton) { element.disabled = false; } }); if (connectButton) { connectButton.classList.remove('d-none'); } if (clientListButton) { clientListButton.classList.add('d-none'); } if (pairingCard) { pairingCard.classList.remove('is-pairing'); } if (pairingProgress) { pairingProgress.classList.add('d-none'); } message.className = 'alert d-none'; message.textContent = ''; if (anotherButton) { anotherButton.classList.remove('btn-secondary'); anotherButton.classList.add('btn-outline-secondary'); anotherButton.setAttribute('aria-label', 'Scan player QR code'); anotherButton.setAttribute('title', 'Scan player QR code'); } if (anotherButtonLabel) { anotherButtonLabel.classList.add('d-none'); } if (manualButton) { manualButton.classList.add('d-none'); } } function codeFromScan(rawValue) { try { var scannedUrl = new URL(String(rawValue || ''), window.location.origin); if (scannedUrl.pathname === '/pairing') { return normalizeCode(scannedUrl.searchParams.get('code')); } } catch (_error) {} return normalizeCode(rawValue); } function applyScannedCode(rawValue) { var code = codeFromScan(rawValue); if (code.length !== codeInputs.length) { scannerMessage.textContent = 'That QR code is not a Pulse Signage pairing code.'; return false; } closeScanner(); resetPairingForm(); setCode(code, 0); return true; } function scanFrame() { if (!scannerVideo || !scannerStream) { return; } if (Date.now() - scannerLastDecodeAt < 400) { scannerFrame = window.requestAnimationFrame(scanFrame); return; } scannerLastDecodeAt = Date.now(); if (!window.BarcodeDetector && typeof window.jsQR === 'function') { var width = scannerVideo.videoWidth; var height = scannerVideo.videoHeight; if (width && height) { traceScanner('frame ' + width + 'x' + height); scannerCanvas.width = width; scannerCanvas.height = height; var context = scannerCanvas.getContext('2d', { willReadFrequently: true }); context.drawImage(scannerVideo, 0, 0, width, height); var result = decodeQrCanvas(scannerCanvas); traceScanner(result ? 'decoded' : 'no QR'); if (result && applyScannedCode(result.data)) { return; } scannerFrame = window.requestAnimationFrame(scanFrame); return; } scannerFrame = window.requestAnimationFrame(scanFrame); return; } scannerVideo.__pairingBarcodeDetector.detect(scannerVideo).then(function (barcodes) { if (barcodes.length) { if (applyScannedCode(barcodes[0].rawValue)) { return; } } scannerFrame = window.requestAnimationFrame(scanFrame); }).catch(function () { scannerFrame = window.requestAnimationFrame(scanFrame); }); } function openScanner() { if (!scanner || !scannerVideo || (!window.BarcodeDetector && typeof window.jsQR !== 'function')) { message.className = 'alert alert-warning'; message.textContent = 'Camera scanning is not supported by this browser.'; return; } scannerMessage.textContent = 'Point your camera at the player QR code.'; traceScanner('open BarcodeDetector=' + Boolean(window.BarcodeDetector) + ' jsQR=' + (typeof window.jsQR === 'function')); scannerVideo.classList.remove('d-none'); scanner.classList.remove('d-none'); scanner.setAttribute('aria-hidden', 'false'); if (window.BarcodeDetector) { scannerVideo.__pairingBarcodeDetector = new window.BarcodeDetector({ formats: ['qr_code'] }); } if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { traceScanner('getUserMedia unavailable'); scannerVideo.classList.add('d-none'); scannerMessage.textContent = 'Use the camera to capture the player QR code.'; if (scannerCapture) { scannerCapture.click(); } return; } navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 }, focusMode: { ideal: 'continuous' } }, audio: false }).then(function (stream) { scannerStream = stream; traceScanner('stream opened'); traceScanner(JSON.stringify(stream.getVideoTracks()[0].getSettings ? stream.getVideoTracks()[0].getSettings() : {})); scannerVideo.srcObject = stream; return scannerVideo.play(); }).then(function () { scannerFrame = window.requestAnimationFrame(scanFrame); }).catch(function () { traceScanner('stream failed'); scannerVideo.classList.add('d-none'); scannerMessage.textContent = 'Use the camera to capture the player QR code.'; if (scannerCapture) { scannerCapture.click(); } }); } if (codeInputs.length) { setCode(codeInputsContainer.getAttribute('data-pairing-code'), 0); codeInputs.forEach(function (input, index) { input.addEventListener('input', function () { var value = normalizeCode(input.value); input.value = value.slice(0, 1); if (value.length > 1) { setCode(value, index); } else { syncCodeField(); if (input.value && index < codeInputs.length - 1) { codeInputs[index + 1].focus(); } } }); input.addEventListener('paste', function (event) { event.preventDefault(); setCode(event.clipboardData ? event.clipboardData.getData('text') : '', index); }); input.addEventListener('keydown', function (event) { if (event.key === 'Backspace' && !input.value && index > 0) { codeInputs[index - 1].focus(); codeInputs[index - 1].value = ''; syncCodeField(); } else if (event.key === 'ArrowLeft' && index > 0) { event.preventDefault(); codeInputs[index - 1].focus(); } else if (event.key === 'ArrowRight' && index < codeInputs.length - 1) { event.preventDefault(); codeInputs[index + 1].focus(); } }); }); } form.addEventListener('submit', function (event) { event.preventDefault(); getClientId(); syncCodeField(); if (!codeField || codeField.value.length !== codeInputs.length) { message.className = 'alert alert-warning'; message.textContent = 'Enter the complete six-character pairing code.'; var firstEmptyInput = codeInputs.filter(function (input) { return !input.value; })[0] || codeInputs[0]; if (firstEmptyInput) { firstEmptyInput.focus(); } return; } message.className = 'alert d-none'; message.textContent = ''; var pairingBody = new URLSearchParams(new FormData(form)).toString(); if (pairingCard) { pairingCard.classList.add('is-pairing'); } if (pairingProgress) { pairingProgress.classList.remove('d-none'); } Array.prototype.slice.call(form.elements).forEach(function (element) { if (element !== anotherButton && element !== manualButton) { element.disabled = true; } }); submitPairing(pairingBody, 0).then(function (result) { var response = result.response; var payload = result.payload; if (!response.ok) { throw new Error(payload && payload.error ? payload.error : 'Unable to pair player.'); } if (payload && payload.queued) { return; } message.className = 'alert alert-success'; message.textContent = 'Pairing saved. The player is loading your screen.'; if (pairingCard) { pairingCard.classList.remove('is-pairing'); } if (pairingProgress) { pairingProgress.classList.add('d-none'); } Array.prototype.slice.call(form.elements).forEach(function (element) { if (element !== anotherButton && element !== manualButton) { element.disabled = true; } }); if (connectButton) { connectButton.classList.add('d-none'); } if (clientListButton) { clientListButton.classList.remove('d-none'); } if (anotherButton) { anotherButton.classList.remove('d-none'); anotherButton.classList.remove('btn-outline-secondary'); anotherButton.classList.add('btn-secondary'); anotherButton.setAttribute('aria-label', 'Pair another screen'); anotherButton.setAttribute('title', 'Pair another screen'); } if (anotherButtonLabel) { anotherButtonLabel.classList.remove('d-none'); } if (manualButton) { manualButton.classList.remove('d-none'); } }).catch(function (error) { if (pairingCard) { pairingCard.classList.remove('is-pairing'); } if (pairingProgress) { pairingProgress.classList.add('d-none'); } Array.prototype.slice.call(form.elements).forEach(function (element) { if (element !== anotherButton && element !== manualButton) { element.disabled = false; } }); message.className = 'alert alert-danger'; message.textContent = error && error.message ? error.message : 'Unable to pair player.'; }); }); if (anotherButton) { anotherButton.addEventListener('click', openScanner); } if (manualButton) { manualButton.addEventListener('click', function () { resetPairingForm(); if (codeInputs[0]) { codeInputs[0].focus(); } }); } if (scannerClose) { scannerClose.addEventListener('click', closeScanner); } if (scannerCapture) { scannerCapture.addEventListener('change', function () { var file = scannerCapture.files && scannerCapture.files[0]; if (!file) { return; } scannerMessage.textContent = 'Reading QR code...'; var image = new Image(); image.onload = function () { if (window.BarcodeDetector) { new window.BarcodeDetector({ formats: ['qr_code'] }).detect(image).then(function (barcodes) { if (!barcodes.length || !applyScannedCode(barcodes[0].rawValue)) { scannerMessage.textContent = 'Could not find a Pulse Signage QR code in that image.'; } }).catch(function () { scannerMessage.textContent = 'Could not read that QR image. Try again.'; }); return; } try { var canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight; var context = canvas.getContext('2d', { willReadFrequently: true }); context.drawImage(image, 0, 0); var result = decodeQrCanvas(canvas); if (!result || !applyScannedCode(result.data)) { scannerMessage.textContent = 'Could not find a Pulse Signage QR code in that image.'; } } catch (_error) { scannerMessage.textContent = 'Could not read that QR image. Try again.'; } }; image.onerror = function () { scannerMessage.textContent = 'Could not read that QR image. Try again.'; }; var reader = new FileReader(); reader.onload = function () { image.src = reader.result; }; reader.onerror = function () { scannerMessage.textContent = 'Could not read that QR image. Try again.'; }; reader.readAsDataURL(file); }); } if (scanner) { scanner.addEventListener('click', function (event) { if (event.target === scanner) { closeScanner(); } }); } }());