// CoRE.Vizion+ — мобильный dashboard. Оба режима через фирменный плеер (.pjs): // Live — только пауза/плей + во весь экран; Архив — перемотка + скорость. // Видео отдаётся сервером уже перекодированным в H.264 (играет на любом телефоне). "use strict"; // фиксированный профиль H.264 для телефона (макс. совместимость) const TR = "codec=h264&profile=baseline&bitrate=2000k&preset=ultrafast&keyint=2&rc=vbr&qp=23"; function pad(n) { return String(n).padStart(2, "0"); } async function api(url) { const r = await fetch(url, { credentials: "same-origin" }); if (r.status === 401) { location.href = "/login"; throw new Error("unauthorized"); } if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); } // ArchivePlayer — общий плеер из static/player.js (подключается ПЕРЕД mobile.js). const M = { cams: [], segs: [], liveId: null, archId: null, date: null, view: "live", player: null, async init() { let data; try { data = await api("/api/cameras"); } catch (e) { return; } this.cams = data.cameras || []; const enabled = this.cams.filter((c) => c.enabled); const lc = document.getElementById("m-live-cam"); const ac = document.getElementById("m-arch-cam"); lc.innerHTML = enabled.map((c) => ``).join(""); ac.innerHTML = this.cams.map((c) => ``).join(""); this.liveId = enabled[0] ? enabled[0].id : null; this.archId = this.cams[0] ? this.cams[0].id : null; lc.onchange = () => { this.liveId = lc.value; this.startLive(); }; ac.onchange = () => { this.archId = ac.value; this.loadArch(); }; const dt = document.getElementById("m-arch-date"); const t = new Date(); this.date = `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())}`; dt.value = this.date; dt.onchange = () => { this.date = dt.value; this.loadArch(); }; // бутерброд-меню сверху: выбор режима (Live / Архив) const drawer = document.getElementById("m-drawer"); const scrim = document.getElementById("m-scrim"); const setMenu = (open) => { drawer.classList.toggle("open", open); scrim.hidden = !open; }; document.getElementById("m-burger").onclick = () => setMenu(!drawer.classList.contains("open")); scrim.onclick = () => setMenu(false); document.querySelectorAll(".m-nav").forEach((b) => { b.onclick = () => { this.showView(b.dataset.view); setMenu(false); }; }); this.showView("live"); }, _kill() { if (this.player) { this.player.destroy(); this.player = null; } }, showView(v) { this.view = v; document.getElementById("m-title").textContent = v === "live" ? "Live" : "Архив"; document.querySelectorAll(".m-nav").forEach((b) => b.classList.toggle("active", b.dataset.view === v)); document.getElementById("view-live").hidden = v !== "live"; document.getElementById("view-arch").hidden = v !== "arch"; this._kill(); if (v === "live") this.startLive(); else this.loadArch(); }, // ── Live ── startLive() { this._kill(); const box = document.getElementById("m-live-box"); if (!this.liveId) { box.innerHTML = '