// assets/js/streamer.js
(async () => {
	const logEl = document.getElementById("log");
	const uaEl = document.getElementById("ua");
	const debugToggle = document.getElementById("wpsl-debug-toggle");
	const startBtn = document.getElementById("start");
	const stopBtn = document.getElementById("stopBtn");
	const preview = document.getElementById("p");
	const recBadge = document.getElementById("wpsl-rec-badge");

	const newStreamBtn = document.getElementById("wpsl-new-stream");
	const box = document.getElementById("wpsl-stream-box");
	const streamIdEl = document.getElementById("wpsl-stream-id");
	const viewerUrlA = document.getElementById("wpsl-viewer-url");
	const createViewerBtn = document.getElementById("wpsl-create-viewer-page");
	const createdPageEl = document.getElementById("wpsl-created-page");
    const streamTitleInput = document.getElementById('wpsl-stream-title');
    const saveTitleBtn = document.getElementById('wpsl-save-title');
    const titleStatus = document.getElementById('wpsl-title-status');
    const allowChat = document.getElementById("wpsl-allow-chat");
    const usersVisibleWrap = document.getElementById('wpsl-users-visible-wrap');
	const viewerLinkRow = document.getElementById("wpsl-viewer-link-row");
    const recTimer = document.getElementById('wpsl-rec-timer');
    // Latencies now come from Settings via WPSL.hlsLatency and WPSL.pollLatency
	const copyLinkBtn = document.getElementById("wpsl-copy-link");
    const saveRecEl = document.getElementById('wpsl-save-recording');
    const videoSrcSel = document.getElementById('wpsl-video-source');
    const audioSrcSel = document.getElementById('wpsl-audio-source');
    // Invitations
    const sendInvitesEl = document.getElementById('wpsl-send-invites');
    const configInvitesBtn = document.getElementById('wpsl-config-invites');
    const inviteModal = document.getElementById('wpsl-invite-modal');
    const inviteClose = document.getElementById('wpsl-invite-close');
    const inviteList = document.getElementById('wpsl-invite-recipients');
    const inviteAddInput = document.getElementById('wpsl-invite-add');
    const inviteAddBtn = document.getElementById('wpsl-invite-add-btn');
    const inviteSubject = document.getElementById('wpsl-invite-subject');
    const inviteMessage = document.getElementById('wpsl-invite-message');
    const invitePreviewBtn = document.getElementById('wpsl-invite-preview');
    const invitePreviewTo = document.getElementById('wpsl-preview-to');
    const inviteApply = document.getElementById('wpsl-invite-apply');
    const inviteStatus = document.getElementById('wpsl-invite-status');
    let INVITE_EMAILS = new Set();

    // Description editor
    const saveDescBtn = document.getElementById('wpsl-save-desc');
    const descStatus = document.getElementById('wpsl-desc-status');
    function getDescContent() {
        try { if (window.tinymce && tinymce.get && tinymce.get('wpsl_desc')) { return String(tinymce.get('wpsl_desc').getContent()||''); } } catch {}
        const ta = document.getElementById('wpsl_desc');
        return String(ta?.value || '');
    }

    // Access controls
    const accessPublic = document.getElementById('wpsl-access-public');
    const accessPassword = document.getElementById('wpsl-access-password');
    const accessPaywall = document.getElementById('wpsl-access-paywall');
    const passwordRow = document.getElementById('wpsl-password-row');
    const paywallRow = document.getElementById('wpsl-paywall-row');
    const passwordEl = document.getElementById('wpsl-password');
    const priceCentsEl = document.getElementById('wpsl-price-cents');
    const currencyEl = document.getElementById('wpsl-price-currency');
    const saveAccessBtn = document.getElementById('wpsl-save-access');
    const accessStatusEl = document.getElementById('wpsl-access-status');
    const requireLoginEl = document.getElementById('wpsl-require-login');
    const allowRegisterEl = document.getElementById('wpsl-allow-register');
    const usersVisibleEl = document.getElementById('wpsl-users-visible');

    const enc = new TextEncoder();

    function show(el) { try { if (el) el.classList.remove('wpsl-hidden'); } catch {} }
    function hide(el) { try { if (el) el.classList.add('wpsl-hidden'); } catch {} }
    function openModal() { if (inviteModal) inviteModal.style.display = ''; }
    function closeModal() { if (inviteModal) inviteModal.style.display = 'none'; }
    function getInviteContent() {
        try {
            if (window.tinymce && tinymce.get && tinymce.get('wpsl-invite-message')) {
                return String(tinymce.get('wpsl-invite-message').getContent() || '');
            }
        } catch {}
        return String(inviteMessage?.value || '');
    }

    function showToast(message, type = 'info') {
        try {
            const wrapId = 'wpsl-toast-wrap';
            let wrap = document.getElementById(wrapId);
            if (!wrap) {
                wrap = document.createElement('div');
                wrap.id = wrapId;
                wrap.style.position = 'fixed';
                wrap.style.zIndex = '100000';
                wrap.style.right = '16px';
                wrap.style.bottom = '16px';
                wrap.style.display = 'flex';
                wrap.style.flexDirection = 'column';
                wrap.style.gap = '8px';
                document.body.appendChild(wrap);
            }
            const el = document.createElement('div');
            el.textContent = String(message || '');
            el.style.background = (type === 'error') ? '#d32f2f' : (type === 'success') ? '#2e7d32' : '#1e88e5';
            el.style.color = '#fff';
            el.style.padding = '10px 12px';
            el.style.borderRadius = '6px';
            el.style.boxShadow = '0 6px 16px rgba(0,0,0,.25)';
            el.style.fontSize = '13px';
            el.style.maxWidth = '320px';
            el.style.wordBreak = 'break-word';
            wrap.appendChild(el);
            setTimeout(() => { try { el.style.transition = 'opacity .4s'; el.style.opacity = '0'; setTimeout(()=> el.remove(), 450); } catch {} }, 2200);
        } catch {}
    }

    function fmtTime(totalSec){
        totalSec = Math.max(0, Math.floor(totalSec));
        const h = Math.floor(totalSec/3600), m = Math.floor((totalSec%3600)/60), s = totalSec%60;
        return (h>0? String(h).padStart(2,'0')+':':'') + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');
    }

    function startRecTimer(){
        try {
            if (!recTimer) return;
            REC_START_TS = Date.now();
            recTimer.style.display = '';
            console.log('WPSL: rec timer show');
            recTimer.textContent = 'REC 00:00';
            if (REC_TICK) clearInterval(REC_TICK);
            REC_TICK = setInterval(() => {
                const secs = (Date.now() - REC_START_TS)/1000;
                recTimer.textContent = 'REC ' + fmtTime(secs);
                try { recTimer.style.background = (secs >= 3600) ? '#b71c1c' : 'rgba(17,17,17,.9)'; } catch {}
            }, 1000);
        } catch {}
    }
    function stopRecTimer(){
        try {
            if (REC_TICK) clearInterval(REC_TICK);
            REC_TICK = null;
            if (recTimer) { recTimer.style.display = 'none'; console.log('WPSL: rec timer hide'); recTimer.textContent = 'REC 00:00'; recTimer.style.background = 'rgba(17,17,17,.9)'; }
        } catch {}
    }

	let STREAM_ID = 0;
	let TOKEN = "";
    let SAVE_REC = false;
    let SEND_INVITES = false;
    let REC_START_TS = 0;
    let REC_TICK = null;

// streamer config
let SEG_DUR = 2;
const WIN = 6;
	const VIDEO_BITRATE = 2_000_000;
	const AUDIO_BITRATE = 96_000;

	function ts() {
		const d = new Date();
		return d.toLocaleTimeString(undefined, { hour12: false }) + "." + String(d.getMilliseconds()).padStart(3, "0");
	}
	function logLine(type, ...args) {
		const msg = args.map(a => typeof a === "string" ? a : JSON.stringify(a, null, 2)).join(" ");
		const line = `[${ts()}] ${type} ${msg}`;
		if (logEl && (!debugToggle || debugToggle.checked)) {
			logEl.textContent += line + "\n";
			logEl.scrollTop = logEl.scrollHeight;
		}
	}
	function clearLog() { if (logEl) logEl.textContent = ""; }

	const ua = navigator.userAgent;
	if (uaEl) uaEl.textContent = ua;

    function updateDebugVisibility() {
        const on = !debugToggle || debugToggle.checked;
        if (uaEl) (on ? show(uaEl) : hide(uaEl));
        if (logEl) (on ? show(logEl) : hide(logEl));
    }
    debugToggle?.addEventListener("change", updateDebugVisibility);
    updateDebugVisibility();

    function updateUsersVisibleVisibility() {
        try {
            if (!usersVisibleWrap) return;
            usersVisibleWrap.style.display = (allowChat && allowChat.checked) ? '' : 'none';
        } catch {}
    }
    if (allowChat) {
        allowChat.addEventListener('change', updateUsersVisibleVisibility);
        updateUsersVisibleVisibility();
    }

	function isIOS() {
		return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
	}
	function hasTrackProcessor() {
		return typeof window.MediaStreamTrackProcessor !== "undefined";
	}

    function showSupportError(msg) {
        try {
            if (supportError) {
                supportError.textContent = msg;
                supportError.style.display = '';
            }
        } catch {}
    }

	// ---- create/load stream
    newStreamBtn?.addEventListener("click", async () => {
		clearLog();
		logLine("INFO", "Creating stream…");

		const fd = new FormData();
		fd.append("action", "wpsl_create_stream");
        if (WPSL?.ajaxNonce) fd.append("nonce", WPSL.ajaxNonce);

		const res = await fetch(WPSL.ajax, { method: "POST", body: fd });
		const json = await res.json();
		if (!json.success) {
			logLine("ERR", "Create stream failed:", json.data || "unknown");
			return;
		}

		STREAM_ID = json.data.stream_id;
		TOKEN = json.data.token;

            box.style.display = "";
			streamIdEl.textContent = String(STREAM_ID);
            const baseUrl = json.data.viewer_url;
            const chat = (allowChat && allowChat.checked) ? '1' : '0';
            const poll = String(WPSL?.pollLatency ?? '1.5');
            const withChat = baseUrl + (baseUrl.includes('?') ? '&' : '?') + 'chat=' + chat + '&poll=' + encodeURIComponent(poll);
            viewerUrlA.href = withChat;
            viewerUrlA.textContent = withChat;

            // Prefill access settings and description for this stream
            try { await prefillAccess(); } catch {}
            try { await prefillDescription(); } catch {}

            // Initialize editable stream title input
            try {
                const defName = `stream_${STREAM_ID}`;
                if (streamTitleInput) streamTitleInput.value = defName;
            } catch {}

            // Populate device lists
            try { await populateDevices(); } catch {}

		logLine("OK", "Stream ready:", STREAM_ID);
	});

    function renderInviteList(users) {
        if (!inviteList) return;
        inviteList.innerHTML = '';
        const frag = document.createDocumentFragment();
        (users||[]).forEach(u => {
            const id = 'wpsl-invite-user-' + (u.id || u.email);
            const wrap = document.createElement('label');
            wrap.style.display = 'flex'; wrap.style.alignItems = 'center'; wrap.style.gap = '8px';
            wrap.style.padding = '4px 2px';
            const cb = document.createElement('input'); cb.type = 'checkbox'; cb.id = id; cb.value = u.email;
            cb.checked = INVITE_EMAILS.has(u.email);
            cb.addEventListener('change', () => { if (cb.checked) INVITE_EMAILS.add(u.email); else INVITE_EMAILS.delete(u.email); });
            const span = document.createElement('span'); span.textContent = `${u.name || ''} <${u.email}>`;
            wrap.appendChild(cb); wrap.appendChild(span);
            frag.appendChild(wrap);
        });
        inviteList.appendChild(frag);
    }

    async function saveStreamTitle() {
        try {
            if (!STREAM_ID || !streamTitleInput) return;
            const title = String((streamTitleInput.value||'').trim());
            if (!title) return;
            if (titleStatus) titleStatus.textContent = 'Saving…';
            const fd = new FormData();
            fd.append('action','wpsl_rename_stream');
            fd.append('stream_id', String(STREAM_ID));
            fd.append('title', title);
            if (WPSL?.ajaxNonce) fd.append('nonce', WPSL.ajaxNonce);
            const res = await fetch(WPSL.ajax, { method:'POST', body: fd });
            const json = await res.json();
            if (!json?.success) throw new Error(String(json?.data||'rename failed'));
            if (titleStatus) { titleStatus.textContent = 'Saved'; setTimeout(()=>{ titleStatus.textContent=''; }, 1000); }
        } catch (e) {
            if (titleStatus) { titleStatus.textContent = 'Save failed'; setTimeout(()=>{ titleStatus.textContent=''; }, 1200); }
        }
    }
    saveTitleBtn?.addEventListener('click', saveStreamTitle);
    streamTitleInput?.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { ev.preventDefault(); saveStreamTitle(); } });
    streamTitleInput?.addEventListener('blur', () => { saveStreamTitle(); });

    async function populateDevices() {
        try {
            if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return;
            const devs = await navigator.mediaDevices.enumerateDevices();
            const vids = devs.filter(d => d.kind === 'videoinput');
            const auds = devs.filter(d => d.kind === 'audioinput');
            const selVid = localStorage.getItem('wpsl_vid_src') || '';
            const selAud = localStorage.getItem('wpsl_aud_src') || '';
            if (videoSrcSel) {
                videoSrcSel.innerHTML = '';
                vids.forEach((d, i) => {
                    const opt = document.createElement('option');
                    opt.value = d.deviceId || '';
                    opt.textContent = d.label || `Camera ${i+1}`;
                    if (opt.value && opt.value === selVid) opt.selected = true;
                    videoSrcSel.appendChild(opt);
                });
            }
            if (audioSrcSel) {
                audioSrcSel.innerHTML = '';
                auds.forEach((d, i) => {
                    const opt = document.createElement('option');
                    opt.value = d.deviceId || '';
                    opt.textContent = d.label || `Microphone ${i+1}`;
                    if (opt.value && opt.value === selAud) opt.selected = true;
                    audioSrcSel.appendChild(opt);
                });
            }
        } catch {}
    }
    videoSrcSel?.addEventListener('change', () => { try { localStorage.setItem('wpsl_vid_src', String(videoSrcSel.value||'')); } catch {} });
    audioSrcSel?.addEventListener('change', () => { try { localStorage.setItem('wpsl_aud_src', String(audioSrcSel.value||'')); } catch {} });

    function updateInviteBtnCount() {
        try { if (configInvitesBtn) configInvitesBtn.textContent = `Add users to be invited (${INVITE_EMAILS.size})`; } catch {}
    }

    sendInvitesEl?.addEventListener('change', () => {
        SEND_INVITES = !!sendInvitesEl.checked;
        if (configInvitesBtn) {
            configInvitesBtn.style.display = SEND_INVITES ? '' : 'none';
            updateInviteBtnCount();
        }
    });
    configInvitesBtn?.addEventListener('click', async () => {
        if (!SEND_INVITES) return;
        try {
            if (inviteStatus) inviteStatus.textContent = '';
            openModal();
            // Fetch users
            const fd = new FormData();
            fd.append('action', 'wpsl_list_users');
            if (WPSL?.ajaxNonce) fd.append('nonce', WPSL.ajaxNonce);
            const res = await fetch(WPSL.ajax, { method:'POST', body: fd });
            const json = await res.json();
            const users = (json && json.success && json.data && Array.isArray(json.data.users)) ? json.data.users : [];
            renderInviteList(users);
        } catch {}
    });
    inviteClose?.addEventListener('click', closeModal);
    inviteAddBtn?.addEventListener('click', () => {
        const v = (inviteAddInput?.value || '').trim();
        if (!v) return; INVITE_EMAILS.add(v); inviteAddInput.value = '';
        const list = Array.from(INVITE_EMAILS).map(e => ({ email:e, name:'' }));
        renderInviteList(list);
        updateInviteBtnCount();
    });
    inviteApply?.addEventListener('click', () => {
        if (inviteStatus) { inviteStatus.textContent = `Selected: ${INVITE_EMAILS.size}`; setTimeout(()=>inviteStatus.textContent='', 1200); }
        closeModal();
        updateInviteBtnCount();
    });

    copyLinkBtn?.addEventListener('click', async () => {
        try {
            if (!viewerUrlA?.href) return;
            await navigator.clipboard.writeText(viewerUrlA.href);
            const old = copyLinkBtn.textContent;
            copyLinkBtn.textContent = 'Copied!';
            setTimeout(() => { copyLinkBtn.textContent = old; }, 1200);
        } catch {}
    });

	createViewerBtn?.addEventListener("click", async () => {
		if (!STREAM_ID) return;

		const fd = new FormData();
		fd.append("action", "wpsl_create_viewer_page");
		fd.append("stream_id", String(STREAM_ID));
        fd.append("allow_chat", (allowChat && allowChat.checked) ? '1' : '0');
        if (pollLatencyEl && pollLatencyEl.value) fd.append('poll', String(pollLatencyEl.value));
        if (WPSL?.ajaxNonce) fd.append("nonce", WPSL.ajaxNonce);

		const res = await fetch(WPSL.ajax, { method: "POST", body: fd });
		const json = await res.json();
		if (!json.success) {
			logLine("ERR", "Create viewer page failed:", json.data || "unknown");
			return;
		}
		createdPageEl.innerHTML = `Created: <a href="${json.data.url}" target="_blank" rel="noopener">${json.data.url}</a>`;
		logLine("OK", "Viewer page created:", json.data.url);
	});

    // moderation removed from admin; handled in viewer UI

	// Proactive capability check
	if (isIOS()) {
		showSupportError("iOS browsers cannot stream (no WebCodecs H.264 encoder). Use Desktop Chrome/Edge or Android Chrome.");
		startBtn?.setAttribute('disabled', 'disabled');
	}
	if (!hasTrackProcessor()) {
		showSupportError("This browser cannot stream (MediaStreamTrackProcessor missing). Use Chrome/Edge.");
		startBtn?.setAttribute('disabled', 'disabled');
	}

	// Update the preview link when chat toggle changes
    allowChat?.addEventListener('change', () => {
		try {
			if (!viewerUrlA?.href) return;
			const url = new URL(viewerUrlA.href, window.location.origin);
            url.searchParams.set('chat', allowChat.checked ? '1' : '0');
            if (pollLatencyEl && pollLatencyEl.value) url.searchParams.set('poll', String(pollLatencyEl.value));
            viewerUrlA.href = url.toString();
            viewerUrlA.textContent = url.toString();
        } catch {}
    });

    // Poll latency is configured in Settings; no live control here

    // Access UI toggle
    function updateAccessRows() {
        const mode = accessPassword?.checked ? 'password' : (accessPaywall?.checked ? 'paywall' : 'public');
        if (passwordRow) passwordRow.style.display = (mode === 'password') ? '' : 'none';
        if (paywallRow) paywallRow.style.display = (mode === 'paywall') ? '' : 'none';
    }
    function updateAllowRegisterVisibility() {
        try {
            const label = allowRegisterEl ? allowRegisterEl.closest('label') : null;
            if (!label) return;
            label.style.display = (requireLoginEl && requireLoginEl.checked) ? '' : 'none';
        } catch {}
    }
    accessPublic?.addEventListener('change', updateAccessRows);
    accessPassword?.addEventListener('change', updateAccessRows);
    accessPaywall?.addEventListener('change', updateAccessRows);
    updateAccessRows();
    requireLoginEl?.addEventListener('change', updateAllowRegisterVisibility);
    updateAllowRegisterVisibility();

    saveAccessBtn?.addEventListener('click', async () => {
        if (!STREAM_ID) return;
        const mode = accessPassword?.checked ? 'password' : (accessPaywall?.checked ? 'paywall' : 'public');
        const fd = new FormData();
        fd.append('action', 'wpsl_save_access');
        fd.append('stream_id', String(STREAM_ID));
        fd.append('access', mode);
        if (mode === 'password' && passwordEl?.value) fd.append('password', passwordEl.value);
        if (mode === 'paywall') {
            if (priceCentsEl?.value) fd.append('price_cents', String(parseInt(priceCentsEl.value||'0', 10)||0));
            if (currencyEl?.value) fd.append('currency', String(currencyEl.value||'').trim());
        }
        if (requireLoginEl) fd.append('require_login', requireLoginEl.checked ? '1' : '0');
        if (allowRegisterEl && requireLoginEl?.checked) fd.append('allow_register', allowRegisterEl.checked ? '1' : '0');
        if (usersVisibleEl) fd.append('users_visible', usersVisibleEl.checked ? '1' : '0');
        if (WPSL?.ajaxNonce) fd.append('nonce', WPSL.ajaxNonce);
        try {
            const res = await fetch(WPSL.ajax, { method:'POST', body: fd });
            const json = await res.json();
            if (!json.success) throw new Error(String(json.data||'failed'));
            if (accessStatusEl) { accessStatusEl.textContent = 'Saved'; setTimeout(()=>{ accessStatusEl.textContent=''; }, 1200); }
        } catch(e) {
            if (accessStatusEl) { accessStatusEl.textContent = 'Error'; setTimeout(()=>{ accessStatusEl.textContent=''; }, 1400); }
        }
    });

    async function saveDescriptionNow() {
        try {
            if (!STREAM_ID) return;
            if (descStatus) { descStatus.textContent = 'Saving…'; }
            const fd = new FormData();
            fd.append('action', 'wpsl_save_description');
            if (WPSL?.ajaxNonce) fd.append('nonce', WPSL.ajaxNonce);
            fd.append('stream_id', String(STREAM_ID));
            fd.append('content', getDescContent());
            const res = await fetch(WPSL.ajax, { method:'POST', body: fd });
            const json = await res.json();
            if (!json?.success) throw new Error(String(json?.data||'save failed'));
            if (descStatus) { descStatus.textContent = 'Saved'; setTimeout(()=>{ descStatus.textContent=''; }, 1000); }
        } catch (e) {
            if (descStatus) { descStatus.textContent = 'Save failed'; setTimeout(()=>{ descStatus.textContent=''; }, 1200); }
        }
    }
    saveDescBtn?.addEventListener('click', saveDescriptionNow);

    async function prefillAccess() {
        if (!STREAM_ID) return;
        const fd = new FormData();
        fd.append('action', 'wpsl_get_access');
        fd.append('stream_id', String(STREAM_ID));
        if (WPSL?.ajaxNonce) fd.append('nonce', WPSL.ajaxNonce);
        const res = await fetch(WPSL.ajax, { method:'POST', body: fd });
        const json = await res.json();
        if (!json.success || !json.data) return;
        const d = json.data;
        try {
            // Access mode
            if (d.access === 'password') { if (accessPassword) accessPassword.checked = true; }
            else if (d.access === 'paywall') { if (accessPaywall) accessPaywall.checked = true; }
            else { if (accessPublic) accessPublic.checked = true; }
            updateAccessRows();
            // Paywall fields
            if (typeof d.price_cents === 'number' && priceCentsEl) priceCentsEl.value = String(d.price_cents);
            if (typeof d.currency === 'string' && currencyEl) currencyEl.value = d.currency;
            // Login requirement
            if (requireLoginEl) requireLoginEl.checked = (String(d.require_login) === '1');
            // Users visibility (default on)
            if (usersVisibleEl) usersVisibleEl.checked = (String(d.users_visible) !== '0');
            // Allow register default reflects current WP setting
            if (allowRegisterEl) allowRegisterEl.checked = !!Number(d.users_can_register || 0);
        } catch {}
    }

	// Rename label for viewer link at runtime (fallback if PHP markup not updated)
	try {
		if (viewerUrlA && viewerUrlA.parentElement?.querySelector('strong')) {
			viewerUrlA.parentElement.querySelector('strong').textContent = 'Preview the live stream:';
		}
	} catch {}

	// ---- REST helpers
	async function postBin(path, body, params) {
		const url = new URL(WPSL.rest + path, window.location.origin);
		Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
		const res = await fetch(url.toString(), { method: "POST", body, cache: "no-store" });
		if (!res.ok) throw new Error(`${path} failed: ${res.status}`);
	}

	async function upload(name, body) {
		return postBin("/upload", body, { stream_id: String(STREAM_ID), token: TOKEN, name });
	}
	async function cleanup() {
		return postBin("/cleanup", new Uint8Array(), { stream_id: String(STREAM_ID), token: TOKEN });
	}

	function u8cat(a, b) {
		const out = new Uint8Array(a.length + b.length);
		out.set(a, 0);
		out.set(b, a.length);
		return out;
	}

	// playlist build
	const segDur = [];
	function buildM3U8(latestSeg) {
		const first = Math.max(1, latestSeg - WIN + 1);
		const target = Math.max(1, Math.ceil(SEG_DUR));
		let s = "";
		s += "#EXTM3U\n";
		s += "#EXT-X-VERSION:7\n";
		s += "#EXT-X-TARGETDURATION:" + target + "\n";
		s += "#EXT-X-MEDIA-SEQUENCE:" + first + "\n";
		s += "#EXT-X-INDEPENDENT-SEGMENTS\n";
		// Hint player to start close to live edge (some players may ignore)
		s += "#EXT-X-START:TIME-OFFSET=-1.0\n";
		s += "#EXT-X-MAP:URI=\"init.mp4\"\n";
		for (let i = first; i <= latestSeg; i++) {
			const d = segDur[i] != null ? segDur[i] : SEG_DUR;
			s += "#EXTINF:" + d.toFixed(3) + ",\n";
			s += `seg_${String(i).padStart(6, "0")}.m4s\n`;
		}
		return s;
	}

    // ---- MediaBunny (LOCAL)
	// You MUST provide assets/js/mediabunny.esm.js (bundled)
	const {
		Output,
		Mp4OutputFormat,
		BufferTarget,
		MediaStreamVideoTrackSource,
		MediaStreamAudioTrackSource,
	} = await import(new URL("./mediabunny.esm.js", import.meta.url).toString());

	// ---- state
	let ms = null, output = null, vsrc = null, asrc = null;
	let init = { ftyp: null, moov: null };
	let pendingMoof = null;
	let seg = 0;
	let stopping = false;

	// serialize publishes so playlist never “goes backwards”
	let publishedSeg = 0;
	let publishChain = Promise.resolve();
	function queuePublish(latestSeg) {
		publishChain = publishChain.then(async () => {
			if (latestSeg <= publishedSeg) return;
			publishedSeg = latestSeg;
			await upload("index.m3u8", enc.encode(buildM3U8(publishedSeg)));
			await upload("latest.txt", enc.encode(String(publishedSeg)));
			logLine("INFO", "Published playlist seg=", publishedSeg);
		}).catch(e => logLine("ERR", "publish:", e?.message || e));
		return publishChain;
	}

    async function uploadRaw(name, data, contentType) {
        try {
            const url = new URL(WPSL?.rest + '/upload', window.location.origin);
            url.searchParams.set('stream_id', String(STREAM_ID||''));
            url.searchParams.set('token', String(TOKEN||''));
            url.searchParams.set('name', String(name||''));
            const res = await fetch(url.toString(), { method:'POST', headers: contentType ? {'Content-Type': contentType} : undefined, body: data });
            if (!res.ok) throw new Error('upload failed ' + res.status);
        } catch (e) { logLine('ERR', 'uploadRaw', name, e?.message||e); }
    }

    async function captureAndUploadPoster() {
        try {
            if (!preview || !preview.videoWidth || !preview.videoHeight) return;
            const maxW = 1280; const ratio = Math.min(1, maxW / preview.videoWidth);
            const w = Math.round(preview.videoWidth * ratio);
            const h = Math.round(preview.videoHeight * ratio);
            const canvas = document.createElement('canvas');
            canvas.width = w; canvas.height = h;
            const ctx = canvas.getContext('2d');
            if (!ctx) return;
            ctx.drawImage(preview, 0, 0, w, h);
            const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.8));
            if (!blob) return;
            await uploadRaw('poster.jpg', blob, 'image/jpeg');
            logLine('OK', 'Uploaded poster.jpg');
        } catch (e) { logLine('ERR', 'poster', e?.message||e); }
    }

    startBtn.onclick = async () => {
        if (!STREAM_ID || !TOKEN) {
            logLine("ERR", "Create/Load a stream first.");
            return;
        }

        // Auto-save description (do not block start)
        try { saveDescriptionNow(); } catch {}

        // apply latency value (segment duration)
        try { const v = parseInt(String(WPSL?.hlsLatency ?? 2), 10); if (Number.isFinite(v)) SEG_DUR = Math.min(6, Math.max(1, v)); } catch {}

		// capability checks
		if (isIOS()) {
			logLine("ERR", "iOS browsers cannot stream (no WebCodecs H.264 encoder). Use Desktop Chrome/Edge or Android Chrome.");
			return;
		}
		if (!hasTrackProcessor()) {
			logLine("ERR", "This browser cannot stream (MediaStreamTrackProcessor missing). Use Chrome/Edge.");
			return;
		}

        clearLog();
        logLine("INFO", "Starting…");

            SAVE_REC = !!(saveRecEl && saveRecEl.checked);

            startBtn.disabled = true;
            stopBtn.disabled = false;
            stopping = false;
            try { if (recBadge) { recBadge.classList.add('is-on'); recBadge.style.display = ''; console.log('WPSL: rec badge ON'); } } catch {}
            try { console.log('WPSL: startRecTimer()'); startRecTimer(); } catch {}
            try { startBtn.classList.add('recording'); } catch {}
            try { if (viewerLinkRow) { setTimeout(() => { viewerLinkRow.style.display = ''; }, 6000); } } catch {}

		publishedSeg = 0;
		publishChain = Promise.resolve();

        // Always start fresh for this stream id
        await cleanup().catch(() => {});
        logLine("INFO", "Cleanup done.");

        const vidDeviceId = (videoSrcSel && videoSrcSel.value) ? String(videoSrcSel.value) : '';
        const audDeviceId = (audioSrcSel && audioSrcSel.value) ? String(audioSrcSel.value) : '';
        const videoConstraints = {
            width: { ideal: 1280 },
            height: { ideal: 720 },
            frameRate: { ideal: 30, max: 30 },
            resizeMode: "crop-and-scale",
        };
        if (vidDeviceId) videoConstraints.deviceId = { exact: vidDeviceId };
        const audioConstraints = audDeviceId ? { deviceId: { exact: audDeviceId }, echoCancellation: true, noiseSuppression: true } : true;
        ms = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: audioConstraints });

        preview.srcObject = ms;

        // Capture a poster shortly after the preview starts
        try {
			const once = () => { setTimeout(captureAndUploadPoster, 400); preview.removeEventListener('playing', once); };
			preview.addEventListener('playing', once);
			// In case 'playing' doesn't fire quickly
			setTimeout(captureAndUploadPoster, 1200);
		} catch {}

		init = { ftyp: null, moov: null };
		pendingMoof = null;
		seg = 0;
		segDur.length = 0;

		vsrc = new MediaStreamVideoTrackSource(ms.getVideoTracks()[0], {
			codec: "avc",
			bitrate: VIDEO_BITRATE,
			keyFrameInterval: SEG_DUR,
			sizeChangeBehavior: "fill",

			onEncoderConfig: (cfg) => {
				// IMPORTANT: DO NOT FORCE cfg.avc.format (iOS playback will break if you force AnnexB)
				logLine("INFO", "EncoderConfig:");
				logLine("INFO", cfg);
			},
		});

		asrc = new MediaStreamAudioTrackSource(ms.getAudioTracks()[0], {
			codec: "aac",
			bitrate: AUDIO_BITRATE,
		});

		output = new Output({
			format: new Mp4OutputFormat({
				fastStart: "fragmented",
				minimumFragmentDuration: SEG_DUR,

				onFtyp: (data) => {
					init.ftyp = new Uint8Array(data);
					logLine("INFO", "ftyp bytes:", init.ftyp.length);
				},

				onMoov: async (data) => {
					init.moov = new Uint8Array(data);
					logLine("INFO", "moov bytes:", init.moov.length);

					const initMp4 = u8cat(init.ftyp, init.moov);

					await upload("init.mp4", initMp4);
					await queuePublish(0);
					logLine("OK", "Uploaded init.mp4");
				},

				onMoof: (data) => {
					pendingMoof = new Uint8Array(data);
				},

				onMdat: async (data) => {
					if (stopping) return;
					if (!pendingMoof) return;

					const fragment = u8cat(pendingMoof, new Uint8Array(data));
					pendingMoof = null;

					seg++;
					segDur[seg] = SEG_DUR;

					const name = `seg_${String(seg).padStart(6, "0")}.m4s`;

					await upload(name, fragment);
					await new Promise(r => setTimeout(r, 200)); // small FS/Apache safety
					await queuePublish(seg);

					logLine("OK", "Uploaded", name, "seg=", seg);
				},
			}),
			target: new BufferTarget(),
		});

		output.addVideoTrack(vsrc);
		output.addAudioTrack(asrc);

        try {
            await output.start();
            logLine("OK", "Output started.");
            try { startRecTimer(); } catch {}
        } catch (e) {
            logLine("ERR", "starting output:", e?.message || e);
            startBtn.disabled = false;
            stopBtn.disabled = true;
            try { if (recBadge) { recBadge.classList.remove('is-on'); recBadge.style.display = 'none'; console.log('WPSL: rec badge OFF'); } } catch {}
            try { startBtn.classList.remove('recording'); } catch {}
            try { console.log('WPSL: stopRecTimer()'); stopRecTimer(); } catch {}
        }
	};

    // Hook into Start to optionally send invites
    if (startBtn) {
        const origOnClick = startBtn.onclick;
        startBtn.onclick = async (ev) => {
            if (SEND_INVITES && INVITE_EMAILS.size > 0) {
                try {
                    const fd = new FormData();
                    fd.append('action', 'wpsl_send_invites');
                    if (WPSL?.ajaxNonce) fd.append('nonce', WPSL.ajaxNonce);
                    fd.append('stream_id', String(STREAM_ID||''));
                    fd.append('subject', String(inviteSubject?.value || 'Live Stream Invitation'));
                    fd.append('message', getInviteContent());
                    fd.append('emails', Array.from(INVITE_EMAILS).join(', '));
                    const res = await fetch(WPSL.ajax, { method:'POST', body: fd });
                    const json = await res.json();
                    if (!json?.success) throw new Error(String(json?.data||'invite failed'));
                    try {
                        const el = document.getElementById('wpsl-invite-result');
                        if (el) {
                            var sent = (json.data && typeof json.data.sent==='number') ? json.data.sent : 0;
                            var failed = (json.data && typeof json.data.failed==='number') ? json.data.failed : 0;
                            var txt = 'Invites sent: ' + sent + ', failed: ' + failed;
                            var errs = json.data && json.data.errors; if (errs && errs.length) txt += ' (last error: ' + errs[errs.length-1] + ')';
                            var fails = [];
                            if (json.data && Array.isArray(json.data.results)) {
                                json.data.results.forEach(function(r){ if (!r.ok && r.email) fails.push(r.email); });
                            }
                            if (fails.length) txt += '; failed emails: ' + fails.slice(0,3).join(', ');
                            el.textContent = txt;
                            showToast(txt, failed > 0 ? 'info' : 'success');
                        }
                    } catch (e) {}
                } catch (e) {
                    // Log but do not block starting
                    logLine('ERR', 'Sending invites:', e?.message || e);
                    showToast('Invite sending failed: ' + (e?.message || e), 'error');
                }
            }
            if (typeof origOnClick === 'function') return origOnClick(ev);
        };
    }

    // Send preview to myself
    invitePreviewBtn?.addEventListener('click', async () => {
        try {
            if (inviteStatus) inviteStatus.textContent = 'Sending preview…';
            const fd = new FormData();
            fd.append('action', 'wpsl_send_invite_preview');
            if (WPSL?.ajaxNonce) fd.append('nonce', WPSL.ajaxNonce);
            fd.append('stream_id', String(STREAM_ID||''));
            fd.append('subject', String(inviteSubject?.value || 'Live Stream Invitation'));
            fd.append('message', getInviteContent());
            if (invitePreviewTo && invitePreviewTo.value) fd.append('to', String(invitePreviewTo.value));
            const res = await fetch(WPSL.ajax, { method:'POST', body: fd });
            const json = await res.json();
            if (!json?.success) throw new Error(String(json?.data||'preview failed'));
            try {
                if (inviteStatus) {
                    var to = (json.data && json.data.to) ? json.data.to : (invitePreviewTo && invitePreviewTo.value) ? invitePreviewTo.value : '';
                    inviteStatus.textContent = 'Preview sent to ' + to;
                }
            } catch(e) {}
            setTimeout(()=>{ if (inviteStatus) inviteStatus.textContent = ''; }, 1500);
        } catch (e) {
            if (inviteStatus) inviteStatus.textContent = 'Preview failed';
            setTimeout(()=>{ if (inviteStatus) inviteStatus.textContent = ''; }, 1500);
        }
    });

	stopBtn.onclick = async () => {
		stopBtn.disabled = true;
		stopping = true;
		logLine("INFO", "Stopping…");

		try {
			ms?.getTracks().forEach(t => t.stop());
			ms = null;

			try { vsrc?.close(); } catch {}
			try { asrc?.close(); } catch {}

			try { await output?.finalize(); } catch {}
			output = null;

            await queuePublish(publishedSeg);

            try {
                if (publishedSeg > 0) {
                    // Build full-length playlist with ENDLIST
                    const target = Math.max(1, Math.ceil(SEG_DUR));
                    let s = '';
                    s += '#EXTM3U\n';
                    s += '#EXT-X-VERSION:7\n';
                    s += '#EXT-X-TARGETDURATION:' + target + '\n';
                    s += '#EXT-X-MEDIA-SEQUENCE:1\n';
                    s += '#EXT-X-INDEPENDENT-SEGMENTS\n';
                    s += '#EXT-X-MAP:URI="init.mp4"\n';
                    for (let i = 1; i <= publishedSeg; i++) {
                        const d = segDur[i] != null ? segDur[i] : SEG_DUR;
                        s += '#EXTINF:' + d.toFixed(3) + ',\n';
                        s += `seg_${String(i).padStart(6,'0')}.m4s\n`;
                    }
                    s += '#EXT-X-ENDLIST\n';
                    const buf = enc.encode(s);
                    // Always finalize index.m3u8 with full content
                    await upload('index.m3u8', buf);
                    if (SAVE_REC) {
                        await upload('vod.m3u8', buf);
                        logLine('OK', 'Saved final playlists (index.m3u8, vod.m3u8).');
                    } else {
                        logLine('OK', 'Saved final playlist (index.m3u8).');
                    }
                } else {
                    await cleanup().catch(() => {});
                    logLine('OK', 'Cleaned up files.');
                }
            } catch (e) {
                logLine('ERR', 'finalize/cleanup:', e?.message || e);
            }

            logLine("OK", "Stopped.");
        } finally {
            startBtn.disabled = false;
            try { if (recBadge) recBadge.style.display = 'none'; } catch {}
            try { stopRecTimer(); } catch {}
            try { startBtn.classList.remove('recording'); } catch {}
        }
	};
})();
