(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 '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(); } })();