mirror of
git://f0xx.org/ac/ac-be-issues
synced 2026-07-29 04:18:08 +03:00
225 lines
7.3 KiB
JavaScript
225 lines
7.3 KiB
JavaScript
(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, '>')
|
||
.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 =
|
||
'<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) + '&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();
|
||
}
|
||
})();
|