MusicMood / static /app.js
Marek4321's picture
fix: przycisk Wlacz kamere gdy autostart zablokowany (SecurityError)
08818fa verified
Raw
History Blame Contribute Delete
10.9 kB
/* ===== MusicMood — logika frontendu ===== */
const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const flash = document.getElementById("flash");
const cameraMsg = document.getElementById("cameraMsg");
const cameraMsgText = document.getElementById("cameraMsgText");
const enableCamBtn = document.getElementById("enableCamBtn");
const cameraSelect = document.getElementById("cameraSelect");
const scanBtn = document.getElementById("scanBtn");
const appPassInput = document.getElementById("appPass");
const statusEl = document.getElementById("status");
const resultsEl = document.getElementById("results");
const tvScreen = document.getElementById("tvScreen");
const tvStatic = document.getElementById("tvStatic");
const tvLed = document.getElementById("tvLed");
let currentStream = null;
const DNI = ["niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"];
/* ---------- kamery ---------- */
async function startStream(deviceId) {
if (currentStream) currentStream.getTracks().forEach((t) => t.stop());
const constraints = {
video: deviceId
? { deviceId: { exact: deviceId }, width: { ideal: 1280 }, height: { ideal: 960 } }
: { width: { ideal: 1280 }, height: { ideal: 960 } },
audio: false,
};
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = currentStream;
cameraMsg.classList.add("hidden");
scanBtn.disabled = false;
}
async function listCameras() {
const devices = await navigator.mediaDevices.enumerateDevices();
const cams = devices.filter((d) => d.kind === "videoinput");
cameraSelect.innerHTML = "";
cams.forEach((cam, i) => {
const opt = document.createElement("option");
opt.value = cam.deviceId;
opt.textContent = cam.label || `Kamera ${i + 1}`;
cameraSelect.appendChild(opt);
});
return cams;
}
async function initCameras() {
if (!navigator.mediaDevices?.getUserMedia) {
cameraMsgText.textContent = "Twoja przeglądarka nie obsługuje dostępu do kamery.";
return;
}
try {
// najpierw poproś o dostęp — bez tego enumerateDevices nie zwraca nazw kamer
await startStream(null);
const cams = await listCameras();
if (cams.length === 0) {
cameraMsgText.textContent = "Nie wykryto żadnej kamery w systemie.";
enableCamBtn.hidden = true;
cameraMsg.classList.remove("hidden");
return;
}
// zaznacz w select aktywną kamerę
const activeId = currentStream.getVideoTracks()[0].getSettings().deviceId;
if (activeId) cameraSelect.value = activeId;
} catch (err) {
// część przeglądarek blokuje kamerę bez gestu użytkownika —
// pokaż przycisk, którego kliknięcie ponawia próbę
cameraMsgText.textContent =
`Kamera nie wystartowała automatycznie (${err.name}${err.message ? ": " + err.message : ""}). ` +
"Kliknij poniżej i zezwól na dostęp.";
enableCamBtn.hidden = false;
cameraMsg.classList.remove("hidden");
}
}
enableCamBtn.addEventListener("click", () => {
cameraMsgText.textContent = "Uruchamianie kamery…";
enableCamBtn.hidden = true;
initCameras();
});
cameraSelect.addEventListener("change", async () => {
try {
await startStream(cameraSelect.value);
} catch {
cameraMsgText.textContent = "Nie udało się uruchomić wybranej kamery.";
cameraMsg.classList.remove("hidden");
}
});
/* ---------- geolokalizacja ---------- */
function getPosition() {
return new Promise((resolve) => {
if (!navigator.geolocation) return resolve(null);
navigator.geolocation.getCurrentPosition(
(pos) => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
() => resolve(null),
{ timeout: 6000, maximumAge: 600000 }
);
});
}
/* ---------- zdjęcie ---------- */
function captureFrame() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0);
// dataURL -> czysty base64
return canvas.toDataURL("image/jpeg", 0.85).split(",")[1];
}
function doFlash() {
flash.classList.add("on");
setTimeout(() => flash.classList.remove("on"), 60);
}
/* ---------- retro logi na ekranie TV ---------- */
const tvLog = document.getElementById("tvLog");
const IDLE_LOGS = [
"MOODTRON BIOS v0.62 ........ OK",
"strojenie anteny do fal nastroju…",
"odkurzanie lamp kineskopu…",
"nasłuch eteru: cisza, tylko szum kosmiczny",
"kalibracja gałki nastroju… ćwierć obrotu w prawo",
"czekam na sygnał… może herbatka?",
"liczę owce w paśmie UHF… doliczyłem do 42",
"prasowanie pasków testowych…",
"ziewam w częstotliwości 50 Hz…",
"sprawdzam, czy ktoś patrzy… chyba tak",
"smarowanie kondensatorów olejem lnianym…",
"trening kineskopu: wdech, wydech…",
"polerowanie elektronów na zapas…",
"nuda… nawet szum ma dziś wolne",
"wciąż gotowy. wciśnij SCAN, nie wstydź się",
];
const SCAN_LOGS = [
"przechwytywanie fotonów twarzy…",
"wysyłanie pikseli do mózgu elektronowego…",
"GPT marszczy brwi nad Twoimi brwiami…",
"mierzenie kącików ust suwmiarką…",
"konsultacja z działem emocji…",
"sprawdzanie ciśnienia (atmosferycznego, nie Twojego)…",
"pytam chmury, jaka pogoda… dosłownie",
"przeszukiwanie szafy z winylami…",
"DJ MoodTron przegląda skrzynki z płytami…",
"strzepywanie kurzu z igły gramofonu…",
"negocjacje z YouTube w sprawie sygnału…",
"dostrajanie tempa do Twojego pulsu…",
"liczenie BPM na liczydle…",
"przesłuchanie sekcji rytmicznej w toku…",
"kalibrowanie nastroju do dnia tygodnia…",
"muzy zwołane na naradę…",
];
let logTimer = null;
let logPool = [];
let logIdx = 0;
function logLine() {
const t = new Date().toLocaleTimeString("pl-PL");
const line = document.createElement("div");
line.textContent = `[${t}] ${logPool[logIdx % logPool.length]}`;
logIdx++;
tvLog.appendChild(line);
while (tvLog.children.length > 9) tvLog.removeChild(tvLog.firstChild);
}
function startLogs(mode) {
clearInterval(logTimer);
logPool = [...(mode === "scan" ? SCAN_LOGS : IDLE_LOGS)]
.sort(() => Math.random() - 0.5);
logIdx = 0;
logLine();
logTimer = setInterval(logLine, mode === "scan" ? 1100 : 2600);
}
function stopLogs() {
clearInterval(logTimer);
logTimer = null;
}
/* ---------- TV (YouTube IFrame API) ---------- */
let ytPlayer = null;
let ytReady = false;
let videoQueue = [];
let queueIndex = 0;
window.onYouTubeIframeAPIReady = () => { ytReady = true; };
function playNextInQueue() {
queueIndex++;
if (queueIndex < videoQueue.length) {
setStatus(`Film niedostępny — próbuję następny wynik (${queueIndex + 1}/${videoQueue.length})…`, "busy");
ytPlayer.loadVideoById(videoQueue[queueIndex]);
} else {
setStatus("Żaden ze znalezionych filmów nie jest dostępny do osadzenia. Kliknij SCAN ponownie.", "error");
}
}
function playOnTV(videoIds) {
videoQueue = videoIds;
queueIndex = 0;
stopLogs();
tvStatic.classList.add("off");
tvLed.classList.add("on");
if (ytPlayer) {
ytPlayer.loadVideoById(videoIds[0]);
return;
}
if (!ytReady || !window.YT) {
setStatus("Player YouTube jeszcze się ładuje — spróbuj za chwilę.", "error");
return;
}
ytPlayer = new YT.Player("ytplayer", {
width: "100%",
height: "100%",
videoId: videoIds[0],
playerVars: { autoplay: 1, rel: 0 },
events: {
onReady: (e) => e.target.playVideo(),
onError: playNextInQueue,
},
});
}
/* ---------- wyniki ---------- */
function setText(id, value) {
document.getElementById(id).textContent = value ?? "—";
}
function showResults(data) {
const o = data.osoba;
setText("rPlec", o?.plec);
setText("rWiek", o?.wiek);
setText("rNastroj", o?.nastroj);
setText("rEmocje", o?.emocje?.join(", "));
const k = data.kontekst || {};
setText("rCzas", `${k.dzien_tygodnia}, ${k.czas_lokalny}`);
const p = k.pogoda;
if (p && typeof p === "object") {
const city = p.miejscowosc ? `${p.miejscowosc}: ` : "";
setText("rPogoda", `${city}${p.opis}, ${p.temperatura_c}°C, wiatr ${p.wiatr_kmh} km/h`);
setText("rCisnienie", p.cisnienie_hpa ? `${Math.round(p.cisnienie_hpa)} hPa` : "—");
} else {
setText("rPogoda", "brak danych");
setText("rCisnienie", "—");
}
const r = data.rekomendacja || {};
setText("rStyl", r.styl);
setText("rTempo", r.tempo);
setText("rRytmika", r.rytmika);
setText("rCharakter", r.charakter);
setText("rUzasadnienie", r.uzasadnienie);
setText("rQuery", data.youtube_query);
resultsEl.hidden = false;
}
function setStatus(text, cls = "") {
statusEl.textContent = text;
statusEl.className = "status " + cls;
}
/* ---------- SCAN ---------- */
scanBtn.addEventListener("click", async () => {
const password = appPassInput.value.trim();
if (!password) {
setStatus("Wpisz najpierw hasło dostępu.", "error");
appPassInput.focus();
return;
}
if (!video.videoWidth) {
setStatus("Kamera jeszcze się nie uruchomiła.", "error");
return;
}
scanBtn.disabled = true;
doFlash();
const imageB64 = captureFrame();
// TV wraca na "szum" z logami analizy; ewentualna muzyka stop
if (ytPlayer && ytPlayer.stopVideo) ytPlayer.stopVideo();
tvLed.classList.remove("on");
tvStatic.classList.remove("off");
startLogs("scan");
setStatus("Pobieranie lokalizacji i analiza zdjęcia (GPT-4o)…", "busy");
const pos = await getPosition();
const now = new Date();
const payload = {
password: password,
image_b64: imageB64,
local_time: now.toLocaleString("pl-PL", { dateStyle: "short", timeStyle: "short" }),
day_of_week: DNI[now.getDay()],
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
lat: pos?.lat ?? null,
lon: pos?.lon ?? null,
};
try {
const resp = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.detail || "Błąd serwera");
showResults(data);
if (data.video_ids && data.video_ids.length > 0) {
playOnTV(data.video_ids);
setStatus("Gotowe! Utwór gra na MoodTronie 📺 (jeśli nie wystartował sam — kliknij ▶ na ekranie TV).");
} else {
setStatus("Analiza gotowa, ale nie udało się znaleźć utworu na YouTube — spróbuj ponownie.", "error");
startLogs("idle");
}
} catch (err) {
setStatus("Błąd: " + err.message, "error");
startLogs("idle");
} finally {
scanBtn.disabled = false;
}
});
/* ---------- start ---------- */
initCameras();
startLogs("idle");
navigator.mediaDevices?.addEventListener?.("devicechange", listCameras);