// 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 = '
Нет доступных камер
'; return; } this.player = new ArchivePlayer(box, { live: true, autohide: false, src: `/api/cameras/${this.liveId}/live.mp4?stream=sub&${TR}` }); }, // ── Архив ── async loadArch() { this._kill(); const box = document.getElementById("m-arch-box"); const list = document.getElementById("m-seglist"); box.innerHTML = '
Выберите запись ниже
'; if (!this.archId || !this.date) { list.innerHTML = ""; return; } const [y, m, d] = this.date.split("-").map(Number); const from = Math.floor(new Date(y, m - 1, d, 0, 0, 0).getTime() / 1000); const to = from + 86400; list.innerHTML = '
Загрузка…
'; let recs = []; try { recs = (await api(`/api/recordings?camera=${this.archId}&from=${from}&to=${to}`)).recordings || []; } catch (e) { list.innerHTML = '
Ошибка загрузки
'; return; } if (!recs.length) { list.innerHTML = '
За выбранный день записей нет
'; return; } this.segs = recs; list.innerHTML = recs.map((r) => { const tt = new Date(r.started_at * 1000); const mins = Math.round((r.duration_s || 0) / 60); return ``; }).join(""); list.onclick = (e) => { const b = e.target.closest(".m-seg"); if (!b) return; list.querySelectorAll(".m-seg").forEach((x) => x.classList.toggle("active", x === b)); this.playSeg(+b.dataset.id); }; }, playSeg(id) { this._kill(); const seg = this.segs.find((r) => r.id === id); const box = document.getElementById("m-arch-box"); this.player = new ArchivePlayer(box, { mode: "transcode", autohide: false, duration: seg ? (seg.duration_s || 0) : 0, srcFor: (t) => `/api/recordings/${id}/play.mp4?${TR}&start=${(t || 0).toFixed(3)}`, }); }, }; document.addEventListener("DOMContentLoaded", () => M.init());