(function () { 'use strict'; function basePath() { return document.body.getAttribute('data-base-path') || ''; } function el(id) { return document.getElementById(id); } function escapeHtml(s) { return String(s || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function detectPlatform() { var ua = navigator.userAgent || ''; if (/Android/i.test(ua)) return 'android'; if (/iPhone|iPad|iPod/i.test(ua)) return 'ios'; if (/Firefox\//i.test(ua)) return 'firefox'; if (/Edg\//i.test(ua)) return 'edge'; if (/Chrome\//i.test(ua)) return 'chrome'; if (/Safari\//i.test(ua)) return 'safari'; return 'desktop'; } function anonId() { var key = 'ac_live_anon_id'; try { var existing = localStorage.getItem(key); if (existing) return existing; var id = 'anon-' + Math.random().toString(36).slice(2) + Date.now().toString(36); localStorage.setItem(key, id); return id; } catch (e) { return 'anon-' + Date.now(); } } function apiUrl() { return basePath() + '/api/live_cast.php'; } function postJoinEvent(sessionId, eventType, direct) { var xhr = new XMLHttpRequest(); xhr.open('POST', apiUrl(), true); xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8'); xhr.send(JSON.stringify({ action: 'join_event', session_id: sessionId, event_type: eventType, platform: detectPlatform(), channel: direct ? 'direct_link' : 'viewer_link', direct: !!direct, anon_id: anonId() })); } function fetchSession(sessionId, cb) { var xhr = new XMLHttpRequest(); xhr.open('GET', apiUrl() + '?action=session&session_id=' + encodeURIComponent(sessionId), true); xhr.onload = function () { var payload = null; try { payload = JSON.parse(xhr.responseText); } catch (e) { cb(null); return; } cb(payload && payload.ok ? payload.session : null); }; xhr.onerror = function () { cb(null); }; xhr.send(); } function formatStatus(session) { if (!session) return 'Unknown'; var st = session.status || ''; if (st === 'live' || st === 'intent') return 'Live'; if (st === 'closed') return 'Ended'; if (st === 'expired') return 'Expired'; return st; } function renderStats(statsEl, stats) { if (!statsEl || !stats) return; statsEl.hidden = false; statsEl.innerHTML = 'Statistics for nerds
' + 'ICE: ' + escapeHtml(stats.ice_state || '—') + ' · sig: ' + escapeHtml(stats.signaling_state || '—') + '
In: ' + (stats.inbound_kbps || 0) + ' kbps · Out: ' + (stats.outbound_kbps || 0) + ' kbps' + '
RTT: ' + (stats.rtt_ms || 0) + ' ms · frames: ' + (stats.frames_decoded || 0) + (stats.frame_w ? '
Frame: ' + stats.frame_w + '×' + stats.frame_h : ''); } function startViewer(sessionId, session, playerEl, statusEl, statsEl) { if (!window.RTCPeerConnection || !window.LiveCastWebRtc) { playerEl.innerHTML = '

WebRTC is not available in this browser.

'; return null; } playerEl.innerHTML = '' + ''; var video = el('live-remote-video'); var nerds = el('live-join-stats'); var conn = window.LiveCastWebRtc.connect({ basePath: basePath(), sessionId: sessionId, role: 'viewer', onStatus: function (msg) { statusEl.textContent = formatStatus(session) + ' — ' + msg; }, onRemoteStream: function (stream) { if (video) { video.srcObject = stream; video.play().catch(function () {}); } statusEl.textContent = formatStatus(session) + ' — playing stream'; }, onStats: function (stats) { renderStats(nerds || statsEl, stats); } }); if (conn && video) conn.setRemoteVideo(video); return conn; } function boot() { var root = el('live-join-app'); if (!root) return; var params = new URLSearchParams(window.location.search); var sessionId = params.get('session_id') || root.getAttribute('data-session-id') || ''; var direct = params.get('direct') === '1' || root.getAttribute('data-direct') === '1'; var role = params.get('role') || (direct ? 'direct' : 'viewer'); var webrtcConn = null; if (!sessionId) { el('live-join-status').textContent = 'Missing session_id in URL.'; return; } postJoinEvent(sessionId, 'open', direct); var statusEl = el('live-join-status'); var metaEl = el('live-join-meta'); var playerEl = el('live-join-player'); var statsEl = el('live-join-stats-panel'); function render(session) { if (!session) { statusEl.textContent = 'Session not found or no longer available.'; return; } var live = session.status === 'live' || session.status === 'intent'; statusEl.textContent = formatStatus(session) + (live ? ' — connecting…' : ''); var owner = session.owner_username || 'host'; var lines = [ 'Host: ' + owner, 'Platform: ' + (session.platform || '—'), 'Video: ' + (session.codec_video || '—'), 'Audio: ' + (session.codec_audio || '—'), 'Bandwidth: ' + (session.bw_mode || '—') ]; if (session.join_short_url) { lines.push('Viewer link: ' + session.join_short_url); } metaEl.innerHTML = lines.map(function (l) { return '
  • ' + escapeHtml(l) + '
  • '; }).join(''); if (direct && /android|ios/i.test(detectPlatform())) { playerEl.innerHTML = '

    Open in the AndroidCast app for direct P2P on LAN.

    ' + '

    androidcast://join?session_id=' + escapeHtml(sessionId) + '&direct=1

    ' + '

    Mobile WebRTC signaling uses the same REST API when the app transport is wired.

    '; postJoinEvent(sessionId, 'join', true); } else if (live) { webrtcConn = startViewer(sessionId, session, playerEl, statusEl, statsEl); postJoinEvent(sessionId, 'join', false); } else { playerEl.innerHTML = '

    Session is not live.

    '; } if (window.AndroidCastAnalytics && window.AndroidCastAnalytics.trackEvent) { window.AndroidCastAnalytics.trackEvent('live_join_view', { session_id: sessionId, direct: direct ? 1 : 0, platform: detectPlatform(), status: session.status || '' }); } } fetchSession(sessionId, render); setInterval(function () { fetchSession(sessionId, function (session) { if (!session) return; if (!(session.status === 'live' || session.status === 'intent')) { statusEl.textContent = formatStatus(session); if (webrtcConn) { webrtcConn.stop(); webrtcConn = null; } } }); }, 30000); window.addEventListener('beforeunload', function () { if (webrtcConn) webrtcConn.stop(); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();