1
0
mirror of git://f0xx.org/ac/ac-be-issues synced 2026-07-29 04:18:08 +03:00
Files
ac-be-issues/public/assets/js/live_join.js
Anton Afanasyeu d5b8c54b5b initial
2026-06-23 12:29:35 +02:00

225 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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();
}
})();