1
0
mirror of git://f0xx.org/ac/ac-be-issues synced 2026-07-29 05:19:06 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-23 12:29:35 +02:00
commit d5b8c54b5b
15 changed files with 2176 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
(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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 =
'<strong>Statistics for nerds</strong><br>'
+ 'ICE: ' + escapeHtml(stats.ice_state || '—')
+ ' · sig: ' + escapeHtml(stats.signaling_state || '—')
+ '<br>In: ' + (stats.inbound_kbps || 0) + ' kbps · Out: ' + (stats.outbound_kbps || 0) + ' kbps'
+ '<br>RTT: ' + (stats.rtt_ms || 0) + ' ms · frames: ' + (stats.frames_decoded || 0)
+ (stats.frame_w ? '<br>Frame: ' + stats.frame_w + '×' + stats.frame_h : '');
}
function startViewer(sessionId, session, playerEl, statusEl, statsEl) {
if (!window.RTCPeerConnection || !window.LiveCastWebRtc) {
playerEl.innerHTML = '<p class="muted">WebRTC is not available in this browser.</p>';
return null;
}
playerEl.innerHTML =
'<video id="live-remote-video" class="live-preview" playsinline autoplay controls></video>'
+ '<div id="live-join-stats" class="live-stats-nerds" hidden></div>';
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 '<li>' + escapeHtml(l) + '</li>';
}).join('');
if (direct && /android|ios/i.test(detectPlatform())) {
playerEl.innerHTML =
'<p class="muted">Open in the AndroidCast app for direct P2P on LAN.</p>' +
'<p><code>androidcast://join?session_id=' + escapeHtml(sessionId) + '&amp;direct=1</code></p>' +
'<p class="muted">Mobile WebRTC signaling uses the same REST API when the app transport is wired.</p>';
postJoinEvent(sessionId, 'join', true);
} else if (live) {
webrtcConn = startViewer(sessionId, session, playerEl, statusEl, statsEl);
postJoinEvent(sessionId, 'join', false);
} else {
playerEl.innerHTML = '<p class="muted">Session is not live.</p>';
}
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();
}
})();